Modern JavaScript'te Asenkron Görevler Yazma

Yayınlanan: 2022-03-10
Kısa özet ↬ Bu makalede, JavaScript'in geçmişteki asenkron yürütme etrafındaki evrimini ve kod yazma ve okuma şeklimizi nasıl değiştirdiğini keşfedeceğiz. Web geliştirmenin başlangıcıyla başlayacağız ve modern asenkron model örneklerine kadar gideceğiz.

JavaScript'in bir programlama dili olarak iki ana özelliği vardır ve her ikisi de kodumuzun nasıl çalışacağını anlamak için önemlidir. Birincisi, senkron doğasıdır, bu, kodun neredeyse siz okurken satır satır çalışacağı ve ikinci olarak, tek iş parçacıklı olduğu, herhangi bir zamanda yalnızca bir komutun yürütüldüğü anlamına gelir.

Dil geliştikçe, eşzamansız yürütmeye izin vermek için sahnede yeni eserler ortaya çıktı; geliştiriciler daha karmaşık algoritmaları ve veri akışlarını çözerken farklı yaklaşımlar denediler ve bu da etraflarında yeni arayüzlerin ve kalıpların ortaya çıkmasına neden oldu.

Senkronize Yürütme ve Gözlemci Modeli

Giriş bölümünde belirtildiği gibi JavaScript, yazdığınız kodu çoğu zaman satır satır çalıştırır. İlk yıllarında bile, dilin bu kuralın istisnaları vardı, ancak bunlar birkaç taneydi ve bunları zaten biliyor olabilirsiniz: HTTP İstekleri, DOM olayları ve zaman aralıkları.

 const button = document.querySelector('button'); // observe for user interaction button.addEventListener('click', function(e) { console.log('user click just happened!'); })

Bir olay dinleyicisi eklersek, örneğin bir öğenin tıklanması ve kullanıcı bu etkileşimi tetiklerse, JavaScript motoru olay dinleyicisi geri çağrısı için bir görevi kuyruğa alır ancak mevcut yığınında mevcut olanı yürütmeye devam eder. Orada bulunan aramalar tamamlandıktan sonra, şimdi dinleyicinin geri aramasını çalıştıracaktır.

Bu davranış, web geliştiricileri için eşzamansız yürütmeye erişen ilk yapıtlar olan ağ istekleri ve zamanlayıcılarla olana benzer.

Bunlar JavaScript'te yaygın eşzamanlı yürütmenin istisnaları olsa da, dilin hala tek iş parçacıklı olduğunu anlamak çok önemlidir ve görevleri sıraya koyabilmesine, eşzamansız olarak çalıştırmasına ve ardından ana iş parçacığına geri dönebilmesine rağmen, yalnızca tek bir kod parçasını yürütebilir. zamanında.

Atlamadan sonra daha fazlası! Aşağıdan okumaya devam edin ↓

Örneğin, bir ağ isteğini kontrol edelim.

 var request = new XMLHttpRequest(); request.open('GET', '//some.api.at/server', true); // observe for server response request.onreadystatechange = function() { if (request.readyState === 4 && request.status === 200) { console.log(request.responseText); } } request.send();

Sunucu geri geldiğinde, onreadystatechange atanan yöntem için bir görev kuyruğa alınır (ana iş parçacığında kod yürütme devam eder).

Not : JavaScript motorlarının görevleri nasıl sıraya koyduğunu ve yürütme dizilerini nasıl ele aldığını açıklamak, ele alınması gereken karmaşık bir konudur ve muhtemelen kendi başına bir makaleyi hak eder. Yine de “What The Heck Is The Event Loop Neyse?” İzlemenizi tavsiye ederim. Philip Roberts tarafından daha iyi anlamanıza yardımcı olmak için.

Bahsedilen her durumda, harici bir olaya yanıt veriyoruz. Belirli bir zaman aralığına ulaşıldı, bir kullanıcı eylemi veya bir sunucu yanıtı. Eşzamansız bir görev oluşturamadık, her zaman erişimimizin dışında gerçekleşen olayları gözlemledik .

Bu şekilde şekillendirilen kodun bu durumda addEventListener arabirimi tarafından daha iyi temsil edilen Observer Pattern olarak adlandırılmasının nedeni budur. Yakında olay yayıcı kitaplıkları veya bu kalıbı açığa çıkaran çerçeveler gelişti.

Node.js ve Olay Yayıcıları

İyi bir örnek, sayfanın kendisini "eşzamansız olay güdümlü JavaScript çalışma zamanı" olarak tanımlayan Node.js'dir, bu nedenle olay yayıcılar ve geri arama birinci sınıf vatandaşlardı. Hatta bir EventEmitter yapıcısı zaten uygulanmıştı.

 const EventEmitter = require('events'); const emitter = new EventEmitter(); // respond to events emitter.on('greeting', (message) => console.log(message)); // send events emitter.emit('greeting', 'Hi there!');

Bu yalnızca eşzamansız yürütme için geçerli bir yaklaşım değildi, aynı zamanda ekosisteminin temel bir modeli ve kuralıydı. Node.js, JavaScript'i farklı bir ortamda, hatta web dışında bile yazmak için yeni bir çağ açtı. Sonuç olarak, yeni dizinler oluşturmak veya dosya yazmak gibi başka asenkron durumlar da mümkün oldu.

 const { mkdir, writeFile } = require('fs'); const styles = 'body { background: #ffdead; }'; mkdir('./assets/', (error) => { if (!error) { writeFile('assets/main.css', styles, 'utf-8', (error) => { if (!error) console.log('stylesheet created'); }) } })

Geri aramaların ilk argüman olarak bir error aldığını fark edebilirsiniz, eğer bir cevap verisi bekleniyorsa, ikinci argüman olarak gider. Buna, yazarların ve katkıda bulunanların kendi paketleri ve kitaplıkları için benimsediği bir kural haline gelen, Önce Hata Geri Çağırma Modeli adı verildi.

Sözler ve Sonsuz Geri Arama Zinciri

Web geliştirme, çözülmesi gereken daha karmaşık sorunlarla karşı karşıya kaldıkça, daha iyi asenkron eserlere duyulan ihtiyaç ortaya çıktı. Son kod parçacığına bakarsak, sayı görevleri arttıkça iyi ölçeklenmeyen tekrarlanan bir geri arama zinciri görebiliriz.

Örneğin, sadece iki adım daha ekleyelim, dosya okuma ve stil ön işleme.

 const { mkdir, writeFile, readFile } = require('fs'); const less = require('less') readFile('./main.less', 'utf-8', (error, data) => { if (error) throw error less.render(data, (lessError, output) => { if (lessError) throw lessError mkdir('./assets/', (dirError) => { if (dirError) throw dirError writeFile('assets/main.css', output.css, 'utf-8', (writeError) => { if (writeError) throw writeError console.log('stylesheet created'); }) }) }) })

Yazdığımız program daha karmaşık hale geldikçe, çoklu geri arama zincirleme ve tekrarlanan hata işleme nedeniyle kodun insan gözü için takip edilmesinin nasıl zorlaştığını görebiliriz.

Sözler, Paketleyiciler ve Zincir Kalıpları

Promises , JavaScript diline yeni ek olarak ilk duyurulduğunda fazla dikkat çekmedi, onlarca yıl önce diğer dillerde benzer uygulamalara sahip oldukları için yeni bir kavram değiller. Gerçek şu ki, ortaya çıktıklarından beri üzerinde çalıştığım projelerin çoğunun anlam ve yapısını çok değiştirdiler.

Promises , geliştiricilerin eşzamansız kod yazmaları için yalnızca yerleşik bir çözüm sunmakla kalmadı, aynı zamanda web spesifikasyonunun fetch gibi sonraki yeni özelliklerinin yapım temeli olarak hizmet veren web geliştirmede yeni bir aşama açtı.

Bir yöntemi bir geri arama yaklaşımından söze dayalı bir yönteme geçirmek, projelerde (kütüphaneler ve tarayıcılar gibi) giderek daha olağan hale geldi ve Node.js bile yavaş yavaş bu yöntemlere geçmeye başladı.

Örneğin, readFile yöntemini saralım:

 const { readFile } = require('fs'); const asyncReadFile = (path, options) => { return new Promise((resolve, reject) => { readFile(path, options, (error, data) => { if (error) reject(error); else resolve(data); }) }); }

Burada bir Promise yapıcısı içinde yürüterek, yöntem sonucu başarılı olduğunda resolve çağırarak ve hata nesnesi tanımlandığında reject geri aramayı gizleriz.

Bir yöntem bir Promise nesnesi döndürdüğünde, başarılı çözümlemesini then bir işlev ileterek takip edebiliriz, argümanı sözün çözüldüğü değerdir, bu durumda data .

Yöntem sırasında bir hata atılırsa, varsa, catch işlevi çağrılır.

Not : Sözlerin nasıl çalıştığını daha derinlemesine anlamak isterseniz, Jake Archibald'ın Google'ın web geliştirme blogunda yazdığı “JavaScript Sözleri: Giriş” makalesini tavsiye ederim.

Artık bu yeni yöntemleri kullanabilir ve geri arama zincirlerinden kaçınabiliriz.

 asyncRead('./main.less', 'utf-8') .then(data => console.log('file content', data)) .catch(error => console.error('something went wrong', error))

Eşzamansız görevler oluşturmak için yerel bir yol ve olası sonuçlarını takip etmek için net bir arayüze sahip olmak, endüstrinin Gözlemci Modelinden çıkmasını sağladı. Söze dayalı olanlar, okunamayan ve hataya açık kodu çözüyor gibiydi.

Daha iyi bir sözdizimi vurgulaması veya daha net hata mesajları kodlama sırasında yardımcı olduğundan, akıl yürütmesi daha kolay bir kod, geliştiricinin onu okuması için daha öngörülebilir hale gelir ve yürütme yolunun daha iyi bir resmi ile olası bir tuzak daha kolay yakalanır.

Promises benimsenmesi toplulukta o kadar küreseldi ki, Node.js, Promise nesnelerini fs.promises içe aktarmak gibi Promise nesnelerini döndürmek için G/Ç yöntemlerinin yerleşik sürümlerini hızla yayınladı.

Hatta Hata Öncelikli Geri promisify izleyen herhangi bir işlevi sarmak ve onu Söz tabanlı bir işleve dönüştürmek için bir söz verme aracı sağladı.

Ancak Vaatler her durumda yardımcı olur mu?

Promises ile yazılmış stil önişleme görevimizi yeniden hayal edelim.

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') readFile('./main.less', 'utf-8') .then(less.render) .then(result => mkdir('./assets') .then(() => writeFile('assets/main.css', result.css, 'utf-8')) ) .catch(error => console.error(error))

Kodda, özellikle şu anda catch 'e güvendiğimiz için hata işleme konusunda açık bir artıklık azalması var, ancak Sözler bir şekilde eylemlerin sıralanmasıyla doğrudan ilgili net bir kod girintisi sağlayamadı.

Bu aslında readFile çağrıldıktan sonra ilk then ifadesinde elde edilir. Bu satırlardan sonra olan şey, önce dizini oluşturabileceğimiz, daha sonra sonucu bir dosyaya yazabileceğimiz yeni bir kapsam oluşturma ihtiyacıdır. Bu, ilk bakışta talimat sırasını belirlemeyi kolaylaştırmadan, girinti ritminde bir kırılmaya neden olur.

Bunu çözmenin bir yolu, bunu ele alan ve yöntemin doğru şekilde birleştirilmesine izin veren özel bir yöntemi önceden pişirmektir, ancak görevi başarmak için ihtiyaç duyduğu şeye zaten sahip gibi görünen bir koda bir karmaşıklık derinliği daha ekleyeceğiz. istiyoruz.

Not : Bu bir örnek programdır ve bazı yöntemlerin kontrolü bizdedir ve bunların tümü bir endüstri konvansiyonuna uygundur, ancak durum her zaman böyle değildir. Daha karmaşık birleştirmeler veya farklı bir şekle sahip bir kitaplığın tanıtılmasıyla kod stilimiz kolayca bozulabilir.

Memnuniyetle, JavaScript topluluğu diğer dil sözdizimlerinden tekrar öğrendi ve eşzamansız görevlerin sıralanmasının eşzamanlı kod kadar kolay veya kolay okunmadığı bu durumlarda çok yardımcı olan bir notasyon ekledi.

zaman uyumsuz ve bekliyor

Bir Promise , yürütme zamanında çözülmemiş bir değer olarak tanımlanır ve bir Promise örneği oluşturmak, bu yapıtın açık bir çağrısıdır.

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') readFile('./main.less', 'utf-8') .then(less.render) .then(result => mkdir('./assets') .then(() => { writeFile('assets/main.css', result.css, 'utf-8') })) .catch(error => console.error(error))

Bir zaman uyumsuz yönteminde, yürütmeye devam etmeden önce bir Promise çözümünü belirlemek için await ayrılmış sözcüğü kullanabiliriz.

Bu sözdizimini kullanarak tekrar veya kod parçacığını inceleyelim.

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less') async function processLess() { const content = await readFile('./main.less', 'utf-8') const result = await less.render(content) await mkdir('./assets') await writeFile('assets/main.css', result.css, 'utf-8') } processLess()

Not : Bugün bir async işlevinin kapsamı dışında await kullanamadığımız için tüm kodumuzu bir yönteme taşımamız gerektiğine dikkat edin .

Bir zaman uyumsuz yöntem bir await ifadesi bulduğunda, devam eden değer veya söz çözülene kadar yürütmeyi durduracaktır.

Zaman uyumsuz/bekleme notasyonu kullanmanın açık bir sonucu vardır, eşzamansız yürütülmesine rağmen, kod senkronmuş gibi görünür, bu da geliştiricilerin görmeye ve düşünmeye daha çok alıştığımız bir şeydir.

Peki ya hata işleme? Bunun için dilde uzun süredir var olan ifadeleri kullanıyoruz, try ve catch .

 const { mkdir, writeFile, readFile } = require('fs').promises; const less = require('less'); async function processLess() { try { const content = await readFile('./main.less', 'utf-8') const result = await less.render(content) await mkdir('./assets') await writeFile('assets/main.css', result.css, 'utf-8') } catch(e) { console.error(e) } } processLess()

İşlemde ortaya çıkan herhangi bir hatanın, catch ifadesinin içindeki kod tarafından işleneceğinden eminiz. Hata işlemeyle ilgilenen merkezi bir yerimiz var, ancak artık okunması ve takip edilmesi daha kolay bir kodumuz var.

Değer döndüren ardışık eylemlere sahip olmak, kod ritmini bozmayan mkdir gibi değişkenlerde saklanmasına gerek yoktur; ayrıca sonraki bir adımda result değerine erişmek için yeni bir kapsam oluşturmaya gerek yoktur.

Sözlerin, hem modern tarayıcılarda hem de Node.js'nin en son sürümlerinde kullanabileceğiniz JavaScript'te async/await gösterimini etkinleştirmek için gerekli, dilde tanıtılan temel bir yapı olduğunu söylemek güvenlidir.

Not : Son zamanlarda JSConf'ta, Node'un yaratıcısı ve ilk katkıcısı Ryan Dahl, erken gelişiminde Vaatlere bağlı kalmadığı için pişman oldu , çünkü Düğüm'ün amacı, Observer modelinin daha iyi hizmet ettiği olay güdümlü sunucular ve dosya yönetimi oluşturmaktı.

Çözüm

Promises'ın web geliştirme dünyasına girişi, kodumuzdaki eylemleri sıraya koyma şeklimizi ve kod yürütmemiz hakkında akıl yürütme şeklimizi ve kitaplıkları ve paketleri nasıl yazdığımızı değiştirmeye geldi.

Ancak geri arama zincirlerinden uzaklaşmanın çözülmesi daha zordur, bence o then bir yöntem iletmek zorunda kalmamız, yıllarca Gözlemci Modeline ve büyük satıcılar tarafından benimsenen yaklaşımlara alıştıktan sonra düşünce treninden uzaklaşmamıza yardımcı olmadı. Node.js gibi toplulukta.

Nolan Lawson'ın Promise birleştirmelerindeki yanlış kullanımlar hakkındaki mükemmel makalesinde dediği gibi, eski geri arama alışkanlıkları zor ölür ! Daha sonra bu tuzaklardan bazılarından nasıl kurtulacağını açıklar.

Zaman uyumsuz görevler oluşturmak için doğal bir yol sağlamak için orta adım olarak Sözlere ihtiyaç duyulduğuna inanıyorum, ancak daha iyi kod kalıpları üzerinde ilerlememize pek yardımcı olmadı, bazen aslında daha uyarlanabilir ve geliştirilmiş bir dil sözdizimine ihtiyacınız var.

JavaScript kullanarak daha karmaşık bulmacaları çözmeye çalışırken, daha olgun bir dile ihtiyaç olduğunu görüyoruz ve daha önce web'de görmeye alışık olmadığımız mimarileri ve kalıpları deniyoruz.

JavaScript yönetimini her zaman web'in dışına genişlettiğimiz ve daha karmaşık bulmacaları çözmeye çalıştığımız için ECMAScript spesifikasyonunun yıllar içinde nasıl görüneceğini hala bilmiyoruz.

Bu bulmacalardan bazılarının daha basit programlara dönüşmesi için dilden tam olarak neye ihtiyacımız olacağını söylemek zor, ancak web ve JavaScript'in bir şeyleri nasıl hareket ettirdiğinden, zorluklara ve yeni ortamlara uyum sağlamaya çalışmasından memnunum. Şu anda JavaScript'in on yıldan fazla bir süre önce bir tarayıcıda kod yazmaya başladığımdan daha eşzamansız ve arkadaşça bir yer olduğunu hissediyorum.

Daha fazla okuma

  • “JavaScript Sözleri: Bir Giriş,” Jake Archibald
  • Bir Bluebird kitaplığı dokümantasyonu olan “Promise Anti-Patterns”
  • Nolan Lawson , “Vaatlerle İlgili Bir Sorunumuz Var”