Image 200
JavaScript’te Adım Adım Asenkron Programlama

Mobil programlama, web programlama, server-side programlama gibi birçok teknolojinin içinde kullandığımız popüler bir programlama dili olan Javascript’te, asenkron programlamayı bir akış içinde anlatmaya çalışacağım. Her bir kısa başlık bir sonraki konu için ön hazırlık niteliğinde olacak.

Şimdiden iyi okumalar dilerim.

Kısaca JavaScript

Öncelikle Javascript’i tanıyalım. ECMAScript(ES) standartlarını uygulayan genel amaçlı bir script dilidir. Tarayıcılar bu sciprtleri çalıştırmak için yine ECMAScript standartlarını uygulayan Javascript Engine’ler sağlar. Bunlardan en ünlüsü Google tarafından geliştirilen V8’dir.

Javascipt single thread çalışır ve iki tane Execution Context yapısı vardır. Birincisi kod çalışmaya başladığında oluşturulan Global Execution Context, diğeri her fonksiyon için, fonksiyonlar çağırıldığında oluşan Function Execution Context’tir. Her context iki tane aşamaya sahiptir. Ortamı hazırlama safhası(creation phase) ve çalıştırma safhası(execution phase). Peki Context’lerin çalışma safhalarında neler yapılır?

Global Execution Context Creation Phase: Global object oluşturulur. Değişkenler ve fonksiyonlar için hafıza yönetilir, web tarayıcılarında this objesi global objeye bağlanır. Tüm değişkenler için başlangıç değeri olan undefined atanır.

Global Execution Context Execution Phase: Kod satır satır çalıştırılır, değişkenlere değerleri atanır ve fonksiyon çağrılmaları yapılır.

Function Execution Context Creation Phase: Arguments objesi fonksiyonun tüm parametrelerine referans olarak oluşturulur. Tüm değişkenler için başlangıç değeri olan undefined atanır.

Function Execution Context Execution Phase: Değişkenlere değerleri atanır ve fonksiyon sonucu Global Context’e dönülür.

Call Stack

Fonksiyonların çağırımı Call Stack yapısı ile yapılır.  İsminden anlaşılacağı üzere Call Stack bir yığındır ve LIFO mantığı ile çalışır. Fonksiyonlar iç içe değilse, stack içerisinde aynı anda tek fonksiyon bulunur.

Şekil 1’deki örnek kodda bulunan fonksiyonların Call Stack içerisindeki çalışma sırası Şekil 2’de gösterilmiştir. Kodun çıktısında, satırlarda sırasıyla 2, 1 ve 3 değerleri olacaktır.

Şekil 1.

Şekil 2.

Blocking, WebAPI, EventQueue, EventLoop

Peki ya Şekil 1’deki fonksiyonlar arasında çalışması uzun sürecek bir fonksiyon olsaydı. func2 fonksiyonunu Şekil 3’deki gibi düzenlediğimizi düşünelim. func2 fonksiyonu ne kadar uzun sürerse sürsün diğer fonksiyonlar func2’nin bitmesini beklemek zorunda olacağından, func2 diğer fonksiyonları bloklayacaktı. Yani tarayıcı herhangi bir başka aktiviteye izin vermeyecekti. Basit bir örnekle anlatmaya çalıştığım bu durum, gerçek hayatta API(Application Programming Interface) çağırılması, Document Object Model(DOM) aktiviteleri, browser aktiviteleri olabilir.

Şekil 3.

Bloklamayı önlemek için bazı fonksiyonların asenkron çalışmasını isteyebiliriz. Daha önce Javascript’in single thread çalıştığından bahsetmiştim ancak bununla birlikte tarayıcıların bize sağladığı Web API’ler var. setTimeout, request’ler, DOM events’lar bu Web API’lerin bir parçasıdır. Önceki örneğimizi Şekil 4’teki gibi biraz daha sadeleştirelim ve setTimeout kullanarak güncelleyelim. 

Şekil 4.

Şekil 4’teki kodun çalışma adımları aşağıda verilmiştir. Çalışma adımları Şekil 5’te görselleştirilmiştir. Burada dikkat edilmesi gereken iki nokta var. Birincisi setTimeout ve console.log birer fonksiyondur ancak gösterimi kolaylaştırmak için genel olarak func1 ve func2 ismiyle anlattım. İkincisi ise setTimeout’un bekleme zamanı 0 olmasına rağmen console’a ilk önce process data, sonra fetch data yazdı. Bu beklediğimiz bir durumdu, asenkronluğu sağladık ama kod sağlıklı çalışmadı. Daha sonra callback ile bu durumu düzelteceğiz.

Çalışma Adımları

  1. func1 Call Stack’e atılır. setTimeout fonksiyonu olduğu için WebAPI’de timer çalıştırılır.
  2. func2 Call Stack’e atılır. Console’da process data yazar.
  3. WebAPI’de timer dolduğunda func1 EventQueue’ye atılır. İsminden anlaşılacağı gibi EventQueue FIFO mantığı ile çalışır. EventLoop, CallStack boş bulduğunda func1’i tekrar Call Stack’e atar ve Console’a fetch data yazılır (3. ve 4. Adım).

Şekil 5.

Callback, Callback Hell

 

Şekil 4’teki asenkron çalışan kod bloğu istediğimiz sırada çalışmamıştı. Bu tür sorunların çözümü için callback fonksiyonlar kullanılır. Callback fonksiyonların herhangi özel bir yapısı yoktur, sadece başka bir fonksiyona parametre olarak verilen fonksiyonlardır. Fonksiyonlar aslında birer obje olduğu için değişkene de atanabilir, parametre olarak da geçilebilir. Başka bir fonksiyonu parametre olarak alan fonksiyon high-order function olarak adlandırılır. Burada soru olarak “Neden tanımlanmış bir fonksiyonu diğer fonksiyon içinde çağırmak yerine parametre olarak vereyim?” sorusu sorulabilir. Bunun özel bir nedeni yok sadece kod tasarımı ile ilgili bir durum. Parametre olarak bir fonksiyon verdiğimizde, parametrenin yerine herhangi bir tanımlanmış fonksiyon ya da anonymous fonksiyon kullanabiliriz bu da kod esnekliğimizi arttırır. filter ve map fonksiyonları içine verdiğimiz anonymous fonksiyonlar buna bir örnek olabilir. Şekil 4’teki kodumuzu callback ile Şekil 6’daki gibi düzenlediğimizde kod bloğu console’a istediğimiz gibi, ilk önce fetch data sonra process data yazdı. Böylece asenkron çalışma sorununu callback ile çözmüş olduk.

Şekil 6.

Callback fonksiyonlar çözüm olmakla beraber iç içe çok fazla callback yapısı kullandığımızda kodun okunurluluğu zorlaştığı için tercih edilmeyebilir. Bu durum Pyramid of Doom ya da Callback Hell olarak adlandırılır. Şekil 7’de de görüldüğü gibi çok fazla iç içe fonksiyonumuz var, bu koda bir de error handling yaptığımızı düşündüğümüzde, kod daha karmaşık hale gelecektir.

Şekil 7.

Promise

Callback Hell gibi sorunları çözmek için Promise yapısı ES6 ile tanıtıldı. Promise bir objedir ve kodun ilerleyişinde bir değer dönebilir.  İki tane fonksiyonu (resolve ve reject) parametre olarak alır. Bu fonksiyonlar sırasıyla başarılı ya da hatalı durumlarda çağırılır. Şekil 8’de görüldüğü gibi Promise yapısının ilk durumu pending’dir ve döndüğü değer undefined’dır, resolve (başarılı) ile döndüğümüzde durumu fullfilled olur ve resolve içindeki değeri döner aynı zamanda reject (hatalı) ile dönüldüğünde durumu rejected olur ve reject içindeki değeri döner.

promise.then() ile döneceği değeri alırız, promise.catch() ile reject ile dönülen hatayı yakalarız. Şekil 8’de hatalı olarak belirtilen kısımda then ile veriyi almak istediğimizde promise objesinin kendisini dönüyor ve yakalanmamış bir hata var uyarısı veriyor ancak catch ile veriyi aldığımız da uyarı almadan dönen değeri görüyoruz.

Şekil 8.

Callback Hell’e neden olan kod bloğunu Şekil 9’daki gibi daha okunabilir hale getirebiliriz. Bu şekilde then yapısını art arda kullanmak, promise chaining olarak adlandırılır. Kod içerisinde promise yapısı varsa kodun bu kısmı jobQueue içine atılır, bu yapı daha önce gördüğümüz eventQueue ile aynı mantıkta çalışır ancak eventQueue’lerden daha öncelikli olarak çalışır.

Şekil 9.

Son olarak ES7 ile birlikte promise yapısını üstüne kurulan async/await fonksiyonları tanıtıldı.

Javascript single-thread iken ‘Nasıl asenkron programlama yapabiliriz?’ sorusuna cevap vermeye çalıştığım bir yazı oldu. Faydalı olduğunu umuyorum. Bir sonraki yazımda görüşmek üzere…

Kaynaklar:

İbrahim Or
Aralık 05 , 2023
Diğer Blog İçerikleri