JavaScript Vaatleri: giriş

Vaatler, ertelenmiş ve eşzamansız hesaplamaları basitleştirir. Vaat, henüz tamamlanmamış bir işlemi temsil eder.

Jake Archibald
Jake Archibald

Geliştiriciler, web geliştirme tarihinin en önemli anlarına kendinizi hazırlayın.

[Drumroll başlar]

JavaScript'te vaatler geldi!

[Patlayan havai fişekler, parıldayan kağıt yağmurları, kalabalık çılgınca dağılıyor]

Bu noktada, aşağıdaki kategorilerden birine girersiniz:

  • İnsanlar etrafınızda tezahürat yapıyor, ancak projenin ne hakkında olduğundan emin değilsiniz. Belki "vaat"in ne olduğundan bile emin değilsin. Omuzlarını silkebilirsin, ama parıltılı kağıt ağırlığı omuzlarında ağır basıyor. Cevabınız evetse dert etmeyin. Bu şeylerle neden ilgilenmem gerektiğini düşünmem çok zaman aldı. Muhtemelen en baştan başlamanız önerilir.
  • Havaya yumruk attınız! Saat yaklaştınız, değil mi? Bu Promise özelliklerini daha önce kullanmıştınız ancak tüm uygulamaların API'leri birbirinden biraz farklı olduğundan rahatsız oluyorsunuz. Resmi JavaScript sürümünün API'sı nedir? Muhtemelen terminolojiyle başlamak istersiniz.
  • Bunu zaten biliyordun ve zıplayıp zıplayanlarla sanki onlar için haber varmış gibi abartıyorsun. Kendi üstünlüğünüzü göstermek için biraz zaman ayırın ve ardından doğrudan API referansına gidin.

Tarayıcı desteği ve çoklu doldurma

Tarayıcı Desteği

  • 32
  • 12
  • 29
  • 8

Kaynak

Eksiksiz vaat uygulaması olan tarayıcıları spesifikasyonlarla uyumluluk düzeyine getirmek veya diğer tarayıcılara ve Node.js'ye vaatler eklemek için polyfill'e (2k gzip ile sıkıştırılmış) bakın.

Karmaşa nedir?

JavaScript tek iş parçacığı biçimindedir. Diğer bir deyişle, iki komut dosyası aynı anda çalışamaz; birbiri ardına çalışmaları gerekir. Tarayıcılarda JavaScript, bir iş parçacığını tarayıcıdan tarayıcıya farklılık gösteren başka öğelerle paylaşır. Ancak genellikle JavaScript; boyama, stilleri güncelleme ve kullanıcı işlemlerini (metni vurgulama ve form kontrolleriyle etkileşim gibi) işlemeyle aynı sırada bulunur. Bunlardan birindeki etkinlik, diğerlerinde gecikmeye neden oluyor.

İnsan olarak, birden çok iş parçacığı kullanıyorsunuz. Birden fazla parmağınızla yazabilir, bir görüşmeyi aynı anda yönlendirip bekletebilirsiniz. Bizim uğraşmamız gereken tek engelleme işlevi hapşırmadır. Bu yüzden hapşırma sırasında tüm mevcut etkinliklerin askıya alınması gerekir. Bu, özellikle araç kullanırken ve konuşmaya çalışırken çok can sıkıcı bir durum. Hapşırtıcı kod yazmak istemezsiniz.

Bu sorunu gidermek için muhtemelen etkinlikler ve geri çağırmalardan yararlanmışsınızdır. Etkinlikler:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Bu hiç hapşırık değildir. Görüntüyü alırız, birkaç işleyici ekleriz. Ardından JavaScript, bu işleyicilerden biri çağrılana kadar yürütülmeyi durdurabilir.

Ne yazık ki yukarıdaki örnekte, etkinliklerin biz onları dinlemeye başlamadan önce gerçekleşmiş olması mümkündür. Bu nedenle, resimlerin "complete" özelliğini kullanarak bu sorunu gidermemiz gerekir:

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

Bu, biz onları dinleme fırsatı bulmadan hatalı olan görüntüleri yakalayamaz. Ne yazık ki DOM bunu yapmamız için bize bir yol sağlamıyor. Ayrıca, bir resim yükleniyor. Bir dizi görselin ne zaman yüklendiğini öğrenmek istediğimiz takdirde işler daha da karmaşık bir hal alır.

Etkinlikler her zaman

Etkinlikler, aynı nesnede (keyup, touchstart vb.) birden çok kez gerçekleşebilecek şeyler için mükemmeldir. Bu etkinliklerde, işleyiciyi eklemeden önce neler olduğu önemli değildir. Ancak, eş zamanlı olmayan başarı/başarısızlık söz konusu olduğunda, ideal olarak şöyle bir şey istersiniz:

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

Vaat edilen budur, ancak daha iyi adlandırma sayesinde. HTML resim öğelerinin vaat döndüren "hazır" bir yöntemi olsaydı şunu yapabilirdik:

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

En temel anlamıyla vaatler, etkinlik işleyicilere biraz benzerdir. Bunun tek istisnası şudur:

  • Bir söz yalnızca bir kez başarılı ya da başarısız olabilir. İki kez başarılı da başarısız da olamaz, başarıdan başarısızlığa veya tam tersi gidemez.
  • Bir söz başarılı veya başarısız olduysa ve daha sonra bir başarılı/başarısız geri çağırma eklerseniz etkinlik daha önce gerçekleşmiş olsa bile doğru geri çağırma çağrılır.

Bu, eşzamansız başarı/başarısızlık için son derece yararlıdır. Çünkü bir şeyin tam olarak ne zaman hazır olduğuyla daha az, sonuca tepki vermekle daha fazla ilgilenirsiniz.

Söz terminolojisi

Domenic Denicola ispatı bu makalenin ilk taslağını okudu ve terminoloji için beni "F" olarak derecelendirdi. Beni gözaltına aldı, States and Fates'i (Eyaletler ve Kaderler) 100 kez kopyalamaya zorladı ve anne-babama endişeli bir mektup yazdı. Buna rağmen, terminoloji konusunda hâlâ pek çok karışıklık var ama temel bilgiler şöyle:

Vaat şunlar olabilir:

  • yerine getirildi - Vaatle ilgili işlem başarılı oldu
  • reddedildi - Vaatle ilgili işlem başarısız oldu
  • beklemede: Henüz karşılanmadı veya reddedilmedi
  • settled: Yerine getirildi veya reddedildi

Spesifikasyon, then yöntemine sahip olması nedeniyle vaat benzeri bir nesneyi tanımlamak için thenable terimini de kullanır. Bu terim bana eski İngiltere Futbol Menajeri Terry Venables'ı hatırladığından, mümkün olduğunca az kullanacağım.

Vaatler JavaScript'te!

Vaatler bir süredir kütüphane olarak kullanılıyor. Örneğin:

Yukarıdaki ve JavaScript vaatleri, Promises/A+ adı verilen yaygın ve standartlaştırılmış bir davranışı paylaşır. Bir jQuery kullanıcısıysanız, bunların Ertelenenler adlı benzer bir davranışı vardır. Ancak Ertelenenler, Promise/A+ uyumlu değildir ve bu nedenle birbirlerinden oldukça farklı ve daha az yararlı olurlar. Bu nedenle dikkatli olun. jQuery'nin de Promise türü vardır ancak bu yalnızca Ertelenenler'in bir alt kümesidir ve aynı sorunlara sahiptir.

Vaat uygulamaları standart bir davranış izlese de genel API'leri farklılık gösterir. JavaScript taahhütleri API'de RSVP.js'ye benzer. Vaat oluşturmak için şunları yapın:

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Vaat oluşturucu, tek bir bağımsız değişkeni, iki parametreli bir geri çağırmayı (çözümle ve reddet) alır. Geri çağırma içinde bir işlem yapın (örneğin eşzamansız olabilir), ardından her şey yolunda giderse resolve'yı, aksi takdirde reddet'i çağırın.

Düz eski JavaScript'teki throw gibi, bir Hata nesnesinin reddedilmesi gelenekseldir ancak zorunlu değildir. Hata nesnelerinin avantajı, yığın izleme yakalayarak hata ayıklama araçlarını daha kullanışlı hale getirmesidir.

Bu sözü şu şekilde kullanırsınız:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then(), başarılı durum için bir geri çağırma ve başarısız durum için başka bir geri çağırma olmak üzere iki bağımsız değişken alır. Her ikisi de isteğe bağlıdır. Dolayısıyla, yalnızca başarılı veya başarısız durum için bir geri çağırma ekleyebilirsiniz.

JavaScript vaatleri DOM'da "Vadeli İşlemler" olarak başlamış, adı "Promise" olarak adlandırılmış ve son olarak JavaScript'e taşınmıştır. Bunların DOM yerine JavaScript'te bulunması harikadır, çünkü Node.js gibi tarayıcı olmayan JS bağlamlarında kullanılabilirler (başka bir soru da bunları temel API'lerinde kullanıp kullanmadıklarıdır).

Bunlar bir JavaScript özelliği olmalarına rağmen, DOM bunları kullanmaktan korkmuyor. Asenkron başarı/hata yöntemlerine sahip tüm yeni DOM API'leri, vadeleri kullanır. Bu durum, Kota Yönetimi, Yazı Tipi Yükleme Etkinlikleri, ServiceWorker, Web MIDI, Akışlar ve daha fazlasında söz konusudur.

Diğer kitaplıklarla uyumluluk

JavaScript taahhüt API'si, then() yöntemine sahip her şeyi vaat edilen gibi (veya thenable sözlü sigh olarak ifade eder) olarak değerlendirir. Bu yüzden, Q sözü döndüren bir kitaplık kullanırsanız yeni JavaScript vaatleri ile sorunsuz çalışacaktır.

Yine de, bahsettiğim gibi, jQuery'nin Ertelenmişleri biraz ...yararlı değil. Neyse ki bunları standart vaatlerin yerine getirebiliyorsunuz. Bunu en kısa sürede yerine getirmenizde fayda var:

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

Burada, jQuery'nin $.ajax işlevi, Ertelenenler döndürür. then() yöntemi olduğundan, Promise.resolve() bunu bir JavaScript sözüne dönüştürebilir. Ancak bazen erteleme işlemleri, geri çağırma işlevlerine birden fazla bağımsız değişken iletir. Örneğin:

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

JS ise ilki hariç tümünü yoksayar:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Neyse ki bu genellikle sizin istediğiniz şeydir veya en azından istediğiniz içeriğe erişmenizi sağlar. Ayrıca, jQuery'nin, Hata nesnelerini retlere geçirme kuralını takip etmediğini unutmayın.

Karmaşık eşzamansız kod kullanımı artık daha kolay

Peki, bir şeyler kodlayalım. Diyelim ki şunları yapmak istiyoruz:

  1. Yükleme durumunu gösteren döner simge başlatın
  2. Bir hikaye için JSON dosyası getirin. Bu biçimde her bölümün adı ve URL'leri verilir.
  3. Sayfaya başlık ekleyin
  4. Her bir bölümü getir
  5. Hikayeyi sayfaya ekleyin
  6. Döner simgeyi durdur

... ama aynı zamanda kullanıcıya süreç boyunca bir şeyler ters giderse de haber verin. Bu noktada döner simgeyi de durdurmak isteriz, aksi takdirde dönmeye devam eder, başı döner ve başka bir kullanıcı arayüzüne kilitlenir.

Elbette, hikaye sunmak için JavaScript kullanamazsınız, HTML olarak sunum daha hızlıdır, ancak şu API'lerle çalışırken oldukça yaygındır: Birden fazla veri getirme, daha sonra, tamamlandığında bir şey yapın.

İlk olarak, ağdan veri alma konusunu ele alalım:

XMLHttpRequest için söz verme

Eski API'ler geriye dönük uyumlu olarak mümkün olduğu takdirde vaatleri kullanacak şekilde güncellenecektir. XMLHttpRequest birincil adaydır ancak bu arada bir GET isteği yapmaya yönelik basit bir işlev yazalım:

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

Şimdi bunu kullanalım:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

Artık manuel olarak XMLHttpRequest yazmadan HTTP istekleri yapabiliyoruz. Bu harika. XMLHttpRequest ürününün rahatsız edici büyük/küçük harf kullanımını ne kadar az görürsem hayatım o kadar mutlu olur.

Zincirleme

then() hikayenin sonu değil. Değerleri dönüştürmek veya ek eşzamansız işlemleri birbiri ardına çalıştırmak için then zincirlerini bir arada kullanabilirsiniz.

Değerleri dönüştürme

Yeni değeri döndürerek değerleri kolayca dönüştürebilirsiniz:

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

Pratik bir örnek olarak geri dönelim:

get('story.json').then(function(response) {
  console.log("Success!", response);
})

Yanıt JSON, ancak şu anda düz metin olarak alıyoruz. JSON responseType kullanmak için get işlevimizi değiştirebiliriz ama vaatler alanında da bu sorunu çözebiliriz:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

JSON.parse() tek bir bağımsız değişken aldığı ve dönüştürülmüş bir değer döndürdüğü için kısayol oluşturabiliriz:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

Aslında, çok kolay bir şekilde getJSON() işlevini yapabiliriz:

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON(), bir URL'yi getirip yanıtı JSON olarak ayrıştıran bir vaat döndürür.

Eşzamansız işlemleri sıraya alma

Ayrıca, eşzamansız işlemleri sırayla çalıştırmak için then zincirleri de oluşturabilirsiniz.

then() geri aramasından bir şeyi geri aldığınızda sihirli bir değnek yaşarsınız. Bir değer döndürürseniz sonraki then() o değerle çağrılır. Ancak, vaat edilen gibi bir öğeyi iade ederseniz bir sonraki then() söz konusu teklifi bekler ve yalnızca söz verilen şey gerçekleştiğinde (başarılı/başarısız) çağrılır. Örneğin:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

Burada story.json için eşzamansız bir istekte bulunuruz. Bu istek, bize isteyeceğimiz bir URL grubunu sağlar, ardından bunlardan ilkini isteriz. İşte bu noktada vaatlerin, basit geri arama kalıpları arasında öne çıkmaya başlaması anlamına gelir.

Bölümleri almak için bir kısayol yöntemi bile oluşturabilirsiniz:

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

getChapter çağrılana kadar story.json indirilmez. Ancak getChapter bir sonraki çağrıldığında hikaye sözünü yeniden kullanırız. Bu nedenle story.json yalnızca bir kez getirilir. Yaşasın!

Hata işleme

Daha önce gördüğümüz gibi then(), biri başarı, biri başarısız olmak (ya da vaatlerde bulunursa yerine getirmek ve reddetmek) olmak üzere iki bağımsız değişken alır:

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

Ayrıca catch() kullanabilirsiniz:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() ile ilgili özel bir şey yok, sadece then(undefined, func) için uygun bir metin ancak daha okunabilir. Yukarıdaki iki kod örneğinin aynı şekilde davranmadığını unutmayın. İkincisi şuna eşdeğerdir:

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

Aradaki fark çok küçüktür, ancak son derece yararlıdır. Vaat edilen retler, ret geri aramasıyla (veya eşdeğer olduğu için catch()) bir sonraki then() öğesine atlanır. then(func1, func2) ile func1 veya func2 çağrılır, her ikisi de çağrılmaz. Ancak then(func1).catch(func2) kullanıldığında, func1 reddederse zincirde ayrı adımlar oldukları için her ikisi de çağrılır. Aşağıdakileri alın:

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

Yukarıdaki akış, normal JavaScript dene/yakalaya çok benzer. Bir "deneme" içinde gerçekleşen hatalar hemen catch() bloğuna gider. Aşağıda bir akış şeması olarak yukarıda görebilirsiniz (çünkü akış şemalarını seviyorum):

Yerine getiren vaatler için mavi çizgileri, reddedenler için kırmızı çizgiyi takip edin.

JavaScript istisnaları ve taahhütleri

Retler, bir taahhüt açıkça reddedildiğinde ve dolaylı olarak oluşturucu geri çağırmasında bir hata olduğunda da gerçekleşir:

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Bu, vaatle ilgili tüm çalışmalarınızı söz oluşturucu geri çağırma içinde yapmak faydalı olur. Böylece hatalar otomatik olarak yakalanıp reddedilir.

Aynı durum then() geri çağırma işleminde yapılan hatalar için de geçerlidir.

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

Uygulamada hata işleme

Hikayemizde ve bölümlerimizde, kullanıcıya bir hatayı göstermek için yakalama işlevini kullanabiliriz:

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

story.chapterUrls[0] getirme başarısız olursa (ör.http 500 veya kullanıcı çevrimdışıysa) sonraki tüm başarılı geri çağırmalar atlanır. Bu geri çağırmalar getJSON() içindeki yanıtı JSON olarak ayrıştırmaya çalışan ve sayfaya bölüm1.html'yi ekleyen geri çağırmayı atlar. Bunun yerine, yakalama geri çağırmasına gider. Sonuç olarak, önceki işlemlerden herhangi biri başarısız olursa sayfaya "Bölüm gösterilemedi" ifadesi eklenir.

JavaScript'in dene-yakala işlevinde olduğu gibi, hata da yakalanır ve sonraki kod devam eder. Bu nedenle döner simge her zaman gizlidir. Bizim istediğimiz de bu. Yukarıdaki kod, şunun engellenmeyen eşzamansız bir sürümü haline gelir:

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

catch() işlemini, hatayı kurtarmadan yalnızca günlük kaydı amacıyla yapmak isteyebilirsiniz. Bunun için hatayı tekrar oluşturmanız yeterlidir. Bunu getJSON() yöntemimizle yapabiliriz:

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

Bir bölümü getirebildik ama hepsini almak istiyoruz. Hadi bunu yapalım.

Paralellik ve sıralılama: İkisinden de en iyi şekilde yararlanın

Eş zamanlı olmadığını düşünmek kolay değildir. Hedeften çıkmakta zorlanıyorsanız kodu eşzamanlıymış gibi yazmayı deneyin. Bu durumda:

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

Bu işe yarıyor! Ancak uygulama senkronize edilir ve içerik indirilirken tarayıcıyı kilitler. Bu işi eşzamansız hale getirmek için, then() ile işlemler birbiri ardına gerçekleşir.

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Peki, bölüm URL'lerini döngüye alıp nasıl sırayla getirebiliriz? Bu yöntem işe yaramaz:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach, eşzamansız modun uyumlu olmadığını amaçlamaktadır. Bu nedenle bölümlerimiz, indirildikleri sırada görünüyordu. Temelde Pulp Kurguda bu şekilde yazılır. Bu Puulp Kurgu değil, şimdi bunu düzeltelim.

Dizi oluşturma

chapterUrls dizimizi sözler dizisine dönüştürmek istiyoruz. Bunu then() özelliğini kullanarak yapabiliriz:

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Size verdiğiniz değere dair bir vaat oluşturan Promise.resolve() ile ilk defa karşılaşıyoruz. Bir Promise örneğini iletirseniz yalnızca döndürür (not: Bu, bazı uygulamaların henüz takip etmediği spesifikasyonda yapılan bir değişikliktir). Vaat gibi bir şey iletirseniz (then() yöntemi vardır) aynı şekilde karşılayan/reddeden gerçek bir Promise oluşturur. Ör. Promise.resolve('Hello') bu şekilde, bu değerle karşılık veren bir vaat oluşturur. Yukarıdaki gibi değer olmadan çağırırsanız "undefined" ile karşılanır.

Bir de Promise.reject(val) var. Bu da, ona verdiğiniz değerle reddeden (veya tanımlanmamış) bir vaat oluşturur.

array.reduce kullanarak yukarıdaki kodu düzeltebiliriz:

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

Bu, önceki örnekle aynı şekilde yapılır, ancak ayrı bir "sequence" değişkenine gerek yoktur. Azaltma geri çağırmamız, dizideki her bir öğe için çağrılır. "sequence" ise ilk seferinde Promise.resolve() değerine ancak geri kalan çağrılar için "dizi", önceki çağrıdan döndürdüğümüz şeydir. array.reduce, bir diziyi tek bir değere indirgemek açısından gerçekten yararlıdır. Bu örnekte, bu bir vaattir.

Hepsini bir araya getirelim:

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

Senkronizasyon sürümünün tamamen eşzamansız bir sürümü var. Ama daha iyisini yapabiliriz. Şu anda sayfamız aşağıdaki gibi indiriliyor:

Tarayıcılar aynı anda birden çok şeyi indirme konusunda oldukça iyidir, bu nedenle bölümleri birbiri ardına indirerek performansı kaybederiz. Yapmak istediğimiz, tüm bunları aynı anda indirmek ve sonra, hepsi ulaştığında onları işlemektir. Neyse ki bunun için bir API var:

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all bir dizi vaatte bulunur ve tümü başarıyla tamamlandığında yerine getirilen bir vaat oluşturur. Verdiğiniz sözlerle aynı sırayla bir dizi sonuç alırsınız (verilen vaatler ne olursa olsun).

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

Bağlantıya bağlı olarak bu işlem, tek tek yüklemeye kıyasla saniyeler daha hızlı olabilir ve ilk denememizden daha az koddur. Bölümler istediğiniz sırayla indirilebilir, ancak ekranda doğru sırada görünür.

Ancak, algılanan performansı iyileştirmeye devam edebiliriz. Birinci bölüm geldiğinde bunu sayfaya eklemeliyiz. Böylece kullanıcı, kalan bölümler gelmeden okumaya başlayabilir. Üçüncü bölüm geldiğinde, kullanıcı ikinci bölümün eksik olduğunu fark etmeyebileceği için onu sayfaya eklemedik. İkinci bölüm geldiğinde ikinci ve üçüncü gibi bölümleri ekleyebiliriz.

Bunu yapmak için aynı anda tüm bölümlerimizin JSON'sini getiriyoruz, ardından bunları belgeye eklemek için bir sıra oluşturuyoruz:

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

İşte oldu, ikisinin de en iyisi! Tüm içeriğin yayınlanması aynı sürede tamamlanır, ancak kullanıcı içeriğin ilk kısmını daha kısa sürede alır.

Bu önemsiz örnekte, tüm bölümler yaklaşık olarak aynı saatte geliyor, ancak her defasında bir bölümü göstermenin avantajı daha büyük bölümlerle abartılacak.

Yukarıdakileri Node.js stili geri çağırmalar veya etkinlikler ile yapmak, yaklaşık olarak iki kat kodlu bir işlemdir, ancak daha da önemlisi, takip edilmesi kolay değildir. Ancak vaatlerin hikayesi sona ermiyor. Diğer ES6 özellikleriyle birleştirildiğinde bu özellikler daha da kolay hale geliyor.

Bonus tur: Genişletilmiş yetenekler

Bu makaleyi ilk başta ben yazdığımdan beri Promises'i kullanabilme özelliği büyük ölçüde arttı. Chrome 55'ten bu yana eşzamansız işlevler, söz tabanlı kodların ana iş parçacığını engellemeden eşzamanlıymış gibi yazılmasına imkan tanıyordu. Bu konu hakkında daha fazla bilgiyi my async functions article bulabilirsiniz. Başlıca tarayıcılarda hem Promises hem de eşzamansız işlevler için yaygın bir destek vardır. Ayrıntıları MDN'nin Promise ve asyncfunction referansında bulabilirsiniz.

Bunu gözden geçirip düzeltmeler/öneriler yapan Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans ve Yutaka Hirano'ya çok teşekkür ederiz.

Bu makalenin çeşitli bölümlerini güncellediği için Mathias Bynens'e de teşekkür ederiz.