[Çeviri] JavaScript Motorları: Nasıl Çalışıyorlar?

Call Stack’ten Promise. Neredeyse Bilmeniz Gereken Her şey

265

Bu makale Valentino Gagliardi tarafından yazılan JavaScript Engines: How Do They Even Work? From Call Stack to Promise, (almost) Everything You Need to Know makalesinin Türkçe çevirisidir. Makaleyi orijinal dilinde okumak için aşağıdaki linki tıklayabilirsiniz.

Çeviri yaparken JavaScript jargonunda yerleşmiş bazı kavramları Türkçe’ye çevirmedim ve bu kavramları özel isimmiş gibi kabul edip ona göre kullandım. Örneğin callback hell kavramını, Callback hell olarak ele aldım. Özel isimlere uygulanan dil bilgisi yapısını uyguladım. kKesme işareti kullanımı vb. Global Memory, Event Loop gibi kavramlarda da benzer süreci işlettim

Başlıyoruz…


Call Stack, Global Memory, Event Loop, Callback Quee’den Promises ve Async/Await de kadar, JavaScript motorlarında kasırgalı bir yolculuk. İyi okumalar!

Tarayıcıların JavaScript kodunu nasıl okuduğunu ve çalıştırdığını hiç merak ettiniz mi? Sihir gibi görünebilir ama Motor’un içerisinde nelerin olduğunu az çok tahmin edebilirsiniz.

JavaScript motorların harika dünyasını açıklayarak, konumuzda derinlere dalmaya başlayalım.

Chrome tarayıcınızda, konsolda Source sekmesine bakınız. Call Stack isimli ilginç bir kutucuğun da bulunduğu bazı kutucuklar göreceksiniz.(Firefox’ta code break point koyduktan sonra Call Stack bilgisini görebilirsiniz):

Call Stak nedir? Bir kaç satır kodu çalıştırmak için bir çok şey var görünüyor.

JavaScript’te kodumuzu derleyen ve çalıştıran büyük bir bileşen vardır: Bu bileşene JavaScript motoru diyoruz. Popüler JavaScript motorları; Google Chrome ve Node.js tarafından kullanılan V8, Firefox için SpiderMonkey ve Safari/Webkit tarafından kullanılan JavaScriptCore’dur.

Günümüzde JavaScript motorları mükemmel mühendislik örneğidir. Ve her bir parçasından bahsetmek imkansızdır. Ama her motorun bizim için çok çalışan bazı bileşenleri vardır.

Bu bileşenler; Global Memory ve Execution Context’le birlike yazdığımız kodun çalışmasını sağlayan Call Stack’tır. Bunlarla tanışmaya hazır mısınız?

JavaScript Motorları ve Global Memory

Belirttiğim gibi; JavaScript aynı anda derlenen ve yorumlanan bir dildir. İster inanın inanmayın, JavaScript motorları; kodu çalıştırmadan bir kaç mikro saniye öncesinde derliyor.

Sihir gibi değil mi? Bu sihire JIT (Just in time compilation) diyoruz. JIT’in kendisi çok büyük bir konudur, bir kitap bile JIT’in nasıl çalıştığını anlatmaya yetmeyebilir. Şimdilik derlemenin arkasında yatan teori kısmını geçiyor ve yine ilginç olan kodun çalışma aşamasına (the execution phase)odaklanıyoruz.

Başlamak için aşağıdaki kodu ele alalım:

“Yukarıdaki kod tarayıcıda hangi süreçlerden geçiyor?” diye sorsaydım ne söylerdiniz? “Tarayıcı kodu okur” veya “Tarayıcı kodu çalıştırır” diye bilirsiniz.

Gerçek durum bundan biraz daha farklı. Öncelikle, tarayıcı bu kod parçacığına bir şey yapmaz. Bu işlemi yapan motordur. JavaScript motoru kodu okur ve en kısa sürede, ilk satırla karşılaşır karşılaşmaz, Global Memory’e bir kaç referans koyar.

Global Memory (Heap de diyebiliriz); JavaScript motorunda, değişkenlerin ve fonksiyon tanımlarının tutulduğu alandır. Örneğimize dönersek, Motor üsteki kodu okuduktan sonra Global Memory iki bağlama(binding) ile doldurulur.

Kodunuzda sadece bir değişken ve bir fonksiyon olsa bile; Tarayıcı veya Node.js’te, JavaScript kodu daha büyük bir ortamda(dünyada) çalışır. Bu ortamda daha önce tanımlanmış ve global diye adlandırdığımız, bir sürü hazır fonksiyon ve değişkenler bulunmaktadır. Global Memory, num ve pow’dan daha gazla bilgi tutacağını unutmayın.

Bu noktada kodumuzda çalışan herhangi bir şey yok. Şimdi fonksiyonumuz çalıştırmayı denersek:

Ne olacak? İşler ilginçleşmeye başlıyor. Bir fonksiyon çalıştırıldığında, JavaScript motoru iki kutuya daha yer açar.

  • Global Execution Context
  • Call Stack

Bu kavramların ne olduklarını bir sonraki bölümde görelim.

JavaScript Motorları: Nasıl Çalışıyorlar? Global Execution Context ve Call Stack

Değişkenlerin ve fonksiyon tanımlarının, Javascrit motoru tarafından nasıl okunduğunu öğrendiniz. Hepsi Global Memory (Heap)de tutulur.

Ama şimdi bir adet JavaScript fonksiyonu çalıştırdık ve motor’un bu durumla ilgilenmesi gerekiyor. Nasıl? JavaScript’te Call Stack diye adlandırılan bir ana bileşen mevcuttur.

Call Stack bir yığın veri yapısıdır. Bunun anlamı; elementler yığına üsten girebilir ve elementin üzerinde başka bir element varsa yığından çıkamazlar (LIFO). JavaScript fonksiyonları tam olarak böyle bir şeydir.

Fonksiyonlar çalıştırıldıktan sonra, işlemi devam eden başka bir fonksiyon varsa, Call Stack’ı terk edemezler. “JavaScript single-threaded” kavramının aklınızda yer etmesi açısından, burası önemlidir.

Ama şimdi örneğimize geri dönelim. Fonksiyon çağrıldığında; motor fonksiyonu Call Stack’in içine ekliyor.

Call Stack’i; Pringles yığını olarak düşünebilirsiniz. En alttaki cipsi yemek için, ilk olarak üsteki tüm cipsleri yemeniz gerekiyor. Şanslıyız ki; basit bir çarpma işlemini hızlıca yapan fonksiyonumuz eş zamanlıdır(synchronous).

Hemen hemen aynı zamanda motor, Global ortamda bulunan ve Javascript kodunun çalıştırıldığı, Global Execution Context’ini ayırır. İlgili alan şu şekilde görünür.

Global Execution Context’i , JavaScript’e ait global fonksiyonların yüzdüğü bir deniz gibi düşünebilirsiniz. Ne hoş! Ama bu sadece hikayenin bir yarısı. Eğer fonksiyonumuz iç içe değişkenler veya birden çok iç fonksiyon içerseydi o zaman ne olurdu?

Aşağıdaki gibi küçük bir varyasyonda bile, JavaScript motoru Local Execution Context’i yaratır.

Dikkat ederseniz; pow fonksiyonunun içine fixed isminde bir değişken ekledim. Bu durumda; fixed değişkenin tutulması için Local Execution Context bir kutu oluşturur.

İç içe küçük kutucuklar çizme konusunda iyi değilimdir. Bunu sizin hayal gücünüze bırakıyorum.

Local Execution Context hemen pow’un yanında, Global Execution Context’in içerisinde yeşilimsi bir kutu olarak görünecektir. Her bir iç içe fonksiyon içinde, iç içe fonksiyonlar için, motor daha fazla Execution Contextsyaratıldığını hayal edin. Bu kutucuklar çok hızlı bir şekilde görünüp kaybolabilir. Matruşka Bebek gibi!

Single-threaded hikayemize dönmeye ne dersiniz. Ne anlama gelmektedir?

JavaScript single-threaded’tır ve diğer komik hikayeler

Fonksiyonlarımızı ele alan bir tane Call Stack olduğundan, JavaScript single-threaded’tır diyebiliriz. Call Stack’ta çalıştırılmayan fonksiyonlar varsa, çalıştırılmasını beklediğimiz fonksiyon Call Stack’ten ayrılamaz.

Eş zamanlı(synchronous) kodlarla işlem yaparken bu bir sorun değildir. Örneğin; iki sayıyı toplayan toplama fonksiyonu eş zamanlı(synchronous) olarak mikro saniyeler içinde çalışır. Ama network çağrıları ve dış dünya ile etkileşim olduğunda ne olur?

Şansımıza ki JavaScript motorları; varsayılan olarak eş zamansız (asynchronous) olacak şekilde tasarlanmışlardır. Her ne kadar, her seferinde bir fonksiyon çalıştırsalar bile, fonksiyonu yavaşlatan harici durumlar vardır. Senaryomuzda tarayıcı. Bunu daha sonra açıklayacağız.

Bu arada; Tarayıcının, JavaScript kodu motor tarafından okunurken bazı şeyleri yüklediğini ve şu adımları takip ettiğini öğrenmiştiniz:

  • Global Memory (Heap)’ye değişkenler ve fonksiyon tanımları yüklenir.
  • Tüm fonksiyon çağrıları Call Stack’e itilir(push).
  • Global fonksiyonların çalıştırıldığı Global Execution Context yaratılır.
  • Bir sürü küçük Local Execution Context’ler yaratılır(İç içe değişkenler ve fonksiyonlar varsa).

Her JavaScript motorunun altında yatan, mekanizmaya ait büyük resmişimdiye kadar görmüş olmalısınız. Bir sonraki bölümde eş zamansız(asynchronouskodların Javascript’te nasıl çalıştığını ve niye böyle bir yolu seçtiğini göreceksiniz.

Asynchronous JavaScript, Callback Queue ve Event Loop

Global Memory, Execution Context ve Call Stack, eş zamanlı(synchronous) JavaScript kodunun tarayıcılarımızda nasıl çalıştığını açıkladık. Yine de bir şeyler eksik. Asynchronous JavaScript kodu çalıştığı zaman ne oluyor?

Asynchronous fonksiyon derken, tamamlanması zaman alan dış sistemlerle ve olan her bir etkileşimden bahsediyorum. REST API çağırmak veya çalışması bir kaç saniye süren bir timer çağırmak. Call Stack’i engellemeyen işlemleri, motorun ve tarayıcının sahip olduğu bileşenlerle halletmesinin bir yolu yok.

Unutmayın Call Stack aynı anda sadece bir fonksiyon çağırır. Hatta bir uzun süren bloklayan bir fonksiyon, tam anlamıyla tarayıcıyı dondurabilir. Şanslıyız ki Javascript motorları çok zeki ve bu tarz işlemleri tarayıcıdan ufak bir yardım alarak çözebilir.

Asynchronous fonksiyon çalıştırdığımız zaman, tarayıcı fonksiyonu alır ve bizim yerimize çalıştırır. Aşağıdaki timer’i ele alalım.

setTimeout fonksiyonu defalarca gördüğünüzden eminim. Bu fonksiyonun JavaScript’te ait olmadığını bilemeyebilirsiniz. JavaScript doğduğunda setTimeout fonksiyonu dilin içine eklenmemiş.

setTimeout fonksiyonu aslında; bize kullanmamız için tarayıcılar tarafından ücretsiz verilen, yararlı bir araç olan Browser APIs’nin bir parçasıdır. Ne hoş! Pratikte ne demektir bu? setTimeout doğrudan tarayıcı tarafından çalıştırılan bir Browser APIs olduğundan, Call Stack’te bir anlığına görünür ve anında kaybolur.

On saniye sonra tarayıcı, verdiğimiz callback fonksiyonunu alır ve Callback Queue’ye iletir. Bu noktada JavaScript motorumuzda iki tane daha kutumuz oldu. Aşağıdaki kodu ele alırsanız:

Çizimimizi aşağıdaki şekilde tamamlarız:

Gördüğünüz gibi, setTimeout tarayıcı içeriğinde(context) çalışır. On saniye sonra timer tetiklenir ve callback fonksiyonu çalışmaya hazırdır. Ama önce Callback Queue’ye gitmesi gerekmektedir. Callback Queue kuyruk veri yapısıdır ve isminden de anlaşılacağı gibi fonksiyonların sıralı bir listesidir.

Her eş zamansız (asynchronous) fonksiyonun, Call Stack’e iletilmeden önce Callback Queue’ye geçmesi gerekmektedir. Peki kim bu fonksiyonları ileri iten? Event Loop adında bir bileşen(component) daha var.

Event Loop’ın simdilik sadece bir görevi var: Call Stack’in boş olup olmadığını kontrol etmek. Callback Queue bekleyen fonksiyon(lar) varsa ve Call Stack boşsa; callback fonksiyonunun Call Stack’a gönderilme zamanı gelmiştir.

Bu işlem yapıldıktan sonra fonksiyon çalıştırılır. JavaScript motorunun asynchronous ve synchronous kodu ile nasıl başa çıktığını gösteren büyük bir resim aşağıda mevcuttur:

callback()’in çalıştırılmaya hazır olduğunu hayal edin. pow() fonksiyonu çalıştırıldıktan sonra Call Stack boşalıyor ve Event Loop callback()fonksiyonunu gönderir. İşte bu. Ne kadar basitleştirsem bile, eğer yukarıdaki görseli anladıysanız, tüm JavaScript’ti anlamaya hazırsınız.

Browser APIs, Callback Queue, ve Event Loop’un eş zamansız (asynchronous) JavaScript’in temel yapı taşları olduğunu unutmayın.

Eğer video izlemekten keyif alıyorsanız; Philip Roberts tarafından hazırlanan “ What the heck is the event loop anyway” videosunu tavsiye ederim. Event Loop için yapılmış en iyi açıklamalardan birisidir.https://www.youtube.com/watch?v=8aGhZQkoFbQ

Durun eş zamansız (asynchronous) JavaScript ile ilgili henüz bir şey yapmadık. Bir sonraki bölümde ES6 Promise’lere daha yakında bakacağız.

Callback hell and ES6 Promises

Callback fonksiyonları JavaScript’te her yerdedir. Eş zamanlı (synchronous) ve Eş zamansız (asynchronous) kodlarda kullanılır. Aşağıdaki map metodunu ele alalım:

mapper fonksiyonu, map’e geçirilen callback fonksiyondur. Üsteki kodu eş zamanlı (synchronous )JavaScript koddur. Ama bunun yerine interval’ı ele alalım.

Yukarıdaki kod eş zamansızdır (asynchronous). Yine de runMeEvery callback fonksiyonunu setInterval fonksiyonuna geçirildiğini görebilirsiniz. Callback’ler JavaScript’tin içinde yaygın olarak kullanılır. Bu yüzden yıllar içinde bir problem ortaya çıkmıştır. Bu problemin adı Callback hell.

JavaScript’teki Calllback hell, iç içe callbacklerin içinde, iç içe callbacklerin geçtiği bir programlama stilidir. JavaScript’tin eş zamansız (asynchronous) doğası gereği, bir çok yazılımcı yıllarca kendini kapana sıkışmış gibi hissetmiştir.

Dürüst olmak gerekirse, Her zaman bağlı kalmaya çalıştığım okunabilir kod ilkesinden ötürü, aşırı büyük callback piramitlerinde hiç çalışmadım. Eğer siz de Callback hell’in içindeyseniz, bu fonksiyonunuzun bir çok iş yaptığının bir işaretidir.

Burada Callback hell’den bahsetmeyeceğim. Eğer bu konuyu merak ediyorsanız callbackhell.com sitesine bakabilirsiniz. Bizim odaklanmak istediğimiz ES6 Promiseler. ES6 Promiseler; Callback hell’i çözmeyi amaçlayan JavaScript eklentisidir. İyi de nedir bu Promise? JavaScript Promise, gelecekteki bir olayın temsil edilmesidir. Bir Promise başarılı bitebilir. Biz buna resolved (fulfilled) diyoruz. Eğer bir Promise hatalı bitmiş ise, buna rejected durum diyoruz. Ayrıca Promise’nin varsayılan bir durumu var. Yeni bir Promise yaratıldığında varsayılan olarak pending durumu başlar. Kendi Promisemizi yaratmak mümkün müdür? Evet. Bir sonraki bölümde nasıl yapıldığına bakalım.

JavaScript Promiseler yaratmak ve çalışmak

Yeni bir Promise yaratmak için; Promise constructor’una callback fonksiyonu geçip çalıştırmak yeterlidir. Callback fonksiyonu resolve ve reject şeklinde iki parametre alabilir. Beş saniye sonra tamamlanan bir Promise oluşturalım(Konsole bu örneği sizde denebilirsiniz.)

Gördüğünüz gibi; resolve , Promise’nin başarılı bir şekilde tamamlanması için çağırdığımız fonksiyondur. Diğer taraftan Promise’yi ret etmek için reject kullanılır.

Dikkat ettiyseniz, birinci örnekte ikinci bir parametre olmadığı için reject’i ihmal edebilirsiniz. Eğer reject’i kullanmak istiyorsanız, resolve’yi ihmal edemezsiniz. Aşağıdaki kod çalışmayacaktır. ve Promise resolved olarak bitecektir.

Şimdi, Promise çok kullanışlı görünmüyor değil mi? Bu örnekler kullanıcı için ekrana bir şeyler yazmıyor. İşleri birazda karıştırmak için biraz veri ekleyelim. resolved ve rejected Promise sonuç(data) dönebilir. Örneğimiz :

Ama yine de herhangi bir veri göremiyoruz. Promise’den veriyi ayıklamak için zincirleme metodu olan then’i çağırmamız gerekiyor. İşin komik yanı, gerçek veriyi almak için then bir tane callback fonksiyonu alır.

Bir JavaScript geliştiricisi olarak yazdığımız kodlarda ve başka yazılımcıların kodunu kullandığınız zaman çoğunlukla Promise’lerle iletişime geçeceksiniz. Bir çok kütüphane geliştiricisi, kodlarını Promise constructor ‘ı ile sarmalayacaktır. Örneğin:

İhtiyaç durumunda, Promise.resolve()’yi kullanarak yeni bir Promise yaratıp resolve edebiliriz.

Tekrar özetlersek: JavaScript Promise gelecekte olacak olaylar için bir yer işaretidir. Olay pending statüsü ile başlar ve ya başarılı olabilir( resolved, fulfilled) yada başarısız olabilir (rejected). Promise veri dönebilir. Ve bu dönen veri then kullanılarak yakalanabilir. Bir sonraki bölümde Promise’de hatalarla nasıl başa çıkıldığını göreceğiz.

ES6 Promise’lerde hata işlemleri

Hata yakalama işlemleri en azından eş zamanlı (synchronous) JavaScript kodunda her zaman kolay olmuştur. Aşağıdaki kodu ele alalım:

Çıktı şu şekilde olacaktır:

Hata iç blokta alındı. Şimdi aynı işlemi eş zamansız (asynchronous) fonksiyonda deneyelim:

Yukarıdaki kod setTimeout kullanıldığı için eş zamansızdır (asynchronous). Kodu çalıştırırsak ne olur.

Yukarıdaki hatalı durumla başa çıkabilmek için callback alan catch’i kullanabiliriz.

Bunun yerine Promise yaratıp rejecting yapmak için Promise.reject() kullanabiliriz.

Tekrar özetlersek: then metodu; Promise başarılı (fullfilled) ise çalışır. Hata olduğundan catch metodu(handler) Promise’yi rejected yapmak için çalışır. Hikaye henüz bitmedi. Bir sonraki bölümde async/await’in try/catch ile nasıl çalıştığını göreceğiz.

ES6 Promises combinators: Promise.all, Promise.allSettled, Promise.any ve arkadaşları

Promise’ler tek başına değillerdir. Promise API’si Promiseleri birbirine bağlamak için bir çok metot içerir. Bunlardan en çok kullanışlı olanı; içinde Promise’lerin bulunduğu listeyi alıp tek bir Promise olarak dönen Promise.all metodudur. Eğer Promise’lerden biri rejected olursa Promise.all’da rejected olur.

Promise.race ise Promise listesindeki Promise’lerden biri yerleşir yerleşmez; resolve yada reject yapar.

V8’in yeni versiyonunda Promise.allSettled ve Promise.any metodları gelecek.

Promise.allSettled ise en ilginç olanı. Promise listesini parametre olarak alır. Eğer içlerinde biri rejected olsa bile kısa devre yapmaz. Promise listesindeki en az birinin yerleşik olup olmadığını kontrol etmek için faydalıdır. Promise.all’ın benzeri gibi düşünebilirsiniz.

ES6 Promises ve Microtask Queue

Bir önceki bölümlerden hatırlarsanız; her JavaScript moturunda eş zamansız (asynchronous) callback fonksiyonu, Call Stack’e gönderilmeden önce Callback Queue’de son bulur. Ama Promise’lerde kullanılan callback fonskiyonunun başka bir kaderi vardır. Bu callbackler, Callback Queue’de Microtask Queue tarafından ele alınır.

Dikkat etmeniz gereken çok garip bir durum var. Microtask Queue; Callback Queue’ye göre önceliklidir. Microtask Queue’den gelen callbackler Event Loop’un kontrollünden sonra Call Stack’e iletilecek callbacklerden önceliklidir.

Altında yatan mekanizma; Jake Archibald tarafından Tasks, microtasks, queues and schedules makalesinde detaylı bir şekilde anlatılmıştır. Okumanızı tavsiye ederim.

JavaScript Motorları: Nasıl Çalışıyorlar? Asynchronous evrim: Promises’de async/await’e

JavaScript her sene, dile yeni iyileştirmeler konusunda hızlı hareket ediyor. Promise’ler bir varış noktası gibi görünüyordu ama ECMAScript 2017 (ES8) ile birlikte async/await doğdu.

async/await; söz dizimsel olarak şeker dediğimiz bir geliştirme. async/await JavaScript’te hiç bir değiklik yapmıyor(Unutmayın, JavaScript eski tarayıcılarla uyumlu olmalı ve çalışan kodu bozmamalı)

Promises’lerin yazılımı ile ilgili yeni bir eş zamansız (asynchronous) kod yazma yöntemidir. Bir örnek yapalım. Öncelikle Promise ile kod yazalım.

Şimdide async/await ile de, kodu okuyan birine göre eş zamanlı (synchronous) kodmuş gibi görünen bu eş zamansız( asynchronous) kod yazabiliriz.

Mantıklı mı? Asıl komik olan ise async her zaman Promise döner. Kimse buna engel olamaz.

Peki ya hatalar konusu nasıl oluyor? Async/await sunduğu nimetlerden birisi’de try / catch kullanımıdır. (an introduction to handling errors in async functions and how to test them). Promise’lerde try/catch kullanımı ile bir örnek yapmıştık:

Aynı kodu async kullanarak tekrar yazalım:

Herkes bu koda tav olmayabilir. try/catch kodda biraz göze batıyor. try/catch kullanırken bu noktada bir gariplik daha var. Kodun iç kısmında hata fırlatılan aşağıdaki kodu düşünelim:

Konsola ne yazılacak? Unutmayın ki try/catch eş zamanlı (synchronous) bir yapıdadır ama eş zamansız ( asynchronous) fonksiyonumuz Promise’dir. İki farklı yolda, giden iki farklı trenler gibi.

Ama hiç bir zaman karşılaşmayacaklar. Yani, getData() tarafından fırlatılan hata hiç bir zaman tetiklenmez . Yukarıdaki kodun sonucu “Catch me if you can” ardından “I will run no matter what!” şeklinde olacaktır.

Şimdi hata beklediğimiz gibi çalışacaktır.

async/await, Javascript’te eş zamansız (asynchronous) kod inşa etmek için en iyi yol gibi görünüyor. Hata yakalama işlemlerinde daha kontrol sahibiyiz ve kodumuz daha temiz görünüyor.

JavaScript Motorları: Nasıl Çalışıyorlar? Toparlama

JavaScript motorları Call Stack, Global Memory, Event Loop, Callback Queue gibi hareketli bir çok parçalara sahiptir. Bütün parçalar eş zamanlı (synchronous) ve eş zamansız (asynchronous) kod yazımı için birlikte çok iyi çalışmaktadır.

JavaScript motorları ingle-threaded’tır. Bunun anlamı; fonksiyonları çalıştırmak için tek Call Stack bulunur. Bu kısıtlama JavaScript’tin eş zamansız( asynchronous) doğasının bir sonucudur. Zaman gerektiren tüm işlemler, harici bir birim tarafından yürütülmelidir( örneğin tarayıcılar) veya callback fonksiyonu tarafından.

Daha basit kod akışı için ECMAScript 2015 ile birlite Promise’ler geldi.Promise eş zamansız( asynchronous) bir nesnedir ve eş zamansız işlemlerin başarılı veya hatalı durumlarını tarif etmek için kullanılır. Fakat geliştirmeler burada durmadı. 2017’de async/await doğdu: Promiseler için, eş zamansız kodları, eş zamanlıymış gibi yazmayı sağlayan bir artistik bir makyaj.

Okuduğunuz için teşekkür ederim ve sayfamı takip etmeye devam edin.


Çeviri notları:

Makalede gözünüze çarpan çeviri hatalarını iletirseniz gerekli düzenlemeleri yaparım. Başka bir çeviride görüşmek üzere.

Yorum yaz

Email adresiniz yayınlanmayacaktır.