JavaScript Vaatleri: giriş

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

Jake Archibald
Jake Archibald

Geliştiriciler, web geliştirme tarihinin en önemli anlarından birine hazırlanıyor.

[Drumroll begins]

JavaScript'te Promises kullanıma sunuldu

[Havai fişekler patlıyor, yukarıdan ışıltılı kağıtlar yağıyor, kalabalık coşuyor]

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

  • Etrafınızdaki insanlar tezahürat ediyor ancak bu kadar gürültünün nedenini anlamıyorsunuz. Belki de "söz"ün ne olduğundan bile emin değilsiniz. Omuzlarınızı silkelemek istiyorsunuz ama ışıltılı kağıdın ağırlığı omuzlarınızda. Bu konuda endişelenmeyin. Bu tür şeylerle neden ilgilenmem gerektiğini anlamam uzun zamanımı aldı. Muhtemelen başlangıçtan başlamak isteyeceksiniz.
  • Havaya yumruk atın. Artık zamanı geldi, değil mi? Bu Promise şeylerini daha önce kullandınız ancak tüm uygulamaların biraz farklı bir API'ye sahip olması sizi rahatsız ediyor. Resmi JavaScript sürümünün API'si nedir? Muhtemelen terminoloji ile başlamak isteyeceksiniz.
  • Bunu zaten biliyorsunuz ve bu bilgiyi yeni öğrenmiş gibi heyecanlananlara gülüyorsunuz. Bir an için kendi üstünlüğünüzün tadını çıkarın, ardından doğrudan API referansına gidin.

Tarayıcı desteği ve polyfill

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

Tam bir söz uygulama özelliği olmayan tarayıcıları spesifikasyonlara uygun hale getirmek veya diğer tarayıcılara ve Node.js'ye söz eklemek için polyfill'i (2k sıkıştırılmış) inceleyin.

Peki Instagram neden bu kadar popüler?

JavaScript tek iş parçacıklıdır. Yani iki komut dosyası aynı anda çalışamaz, sırayla çalışması gerekir. Tarayıcılarda JavaScript, tarayıcıdan tarayıcıya değişen bir sürü başka şeyle birlikte bir iş parçacığını paylaşır. Ancak JavaScript genellikle boyama, stilleri güncelleme ve kullanıcı işlemlerini (ör. metin vurgulama ve form kontrolleriyle etkileşim) işleme ile aynı kuyruktadır. Bu öğelerden birindeki etkinlik diğerlerini geciktirir.

İnsan olarak çoklu iş parçacığına sahipsiniz. Birden fazla parmağınızla yazabilir, aynı anda araba kullanabilir ve sohbet edebilirsiniz. Tek engelleme işlevi hapşırmadır. Hapşırma süresince mevcut tüm etkinlikler askıya alınmalıdır. Bu durum, özellikle araba kullanırken ve konuşmaya çalışırken oldukça can sıkıcıdır. Hapşırtan kod yazmak istemezsiniz.

Bunu aşmak için muhtemelen etkinlikleri ve geri aramaları kullanmış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şırtıcı değil. Resmi alırız, birkaç dinleyici ekleriz. Ardından, bu dinleyicilerden biri çağrılana kadar JavaScript'in yürütülmesi durdurulabilir.

Maalesef yukarıdaki örnekte, etkinlikler biz bunları dinlemeye başlamadan önce gerçekleşmiş olabilir. Bu nedenle, resimlerin "complete" özelliğini kullanarak bu sorunu çözmemiz 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 yöntem, dinleme fırsatı bulmadan önce hata veren resimleri yakalamaz. Maalesef DOM, bunu yapmamıza olanak tanımaz. Ayrıca, bu işlem bir resmi yüklüyor. Bir dizi resmin ne zaman yüklendiğini bilmek istediğimizde işler daha da karmaşık hale gelir.

Etkinlikler her zaman en iyi yöntem olmayabilir

Etkinlikler, aynı nesnede birden çok kez gerçekleşebilecek şeyler için idealdir (keyup, touchstart vb.). Bu etkinliklerde, dinleyiciyi eklemeden önce ne olduğuyla pek ilgilenmezsiniz. Ancak, asenkron başarı/başarısızlık söz konusu olduğunda ideal olarak şuna benzer bir şey istersiniz:

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

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

Bu, daha iyi adlandırma ile vaatlerin yaptığı şeydir. HTML resim öğelerinde bir söz döndüren "ready" 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 düzeyde, sözler etkinlik işleyicilere biraz benzer ancak şu farklar vardır:

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

Bu, özellikle eşzamansız başarı/başarısızlık için son derece yararlıdır. Çünkü bir şeyin tam olarak ne zaman kullanıma sunulduğuyla değil, sonuçla ilgilenirsiniz.

Promise terminolojisi

Domenic Denicola, bu makalenin ilk taslağını okuyarak terimler konusunda bana "F" notunu verdi. Beni okulda alıkoydu, 100 kez States and Fates (Devletler ve Kaderler) adlı kitabı kopyalamaya zorladı ve aileme endişeli bir mektup yazdı. Buna rağmen, terminolojiyi hâlâ çok karıştırıyorum ancak temel bilgiler şöyle:

Bir söz şunlardan biri olabilir:

  • fulfilled: Sözle ilgili işlem başarıyla tamamlandı.
  • rejected: Sözle ilgili işlem başarısız oldu.
  • beklemede: Henüz karşılanmadı veya reddedilmedi.
  • Ödendi: Karşılandı veya reddedildi.

Spesifikasyon, then yöntemi olan ve söz benzeri bir nesneyi tanımlamak için thenable terimini de kullanır. Bu terim bana eski İngiltere Milli Takımı Teknik Direktörü Terry Venables'ı hatırlatıyor. Bu nedenle, bu terimi mümkün olduğunca az kullanacağım.

JavaScript'te sözler

Vaatler, bir süredir şu gibi kitaplıklar şeklinde kullanılıyor:

Yukarıdaki ve JavaScript vaatleri, Promises/A+ adı verilen ortak ve standartlaştırılmış bir davranışı paylaşır. jQuery kullanıcısıysanız benzer bir özellik olan Deferreds'i kullanabilirsiniz. Ancak Deferred'lar Promise/A+ ile uyumlu olmadığından biraz farklı ve daha az kullanışlıdır. Bu nedenle dikkatli olun. jQuery'de de bir Promise türü vardır ancak bu, Deferred'ın yalnızca bir alt kümesidir ve aynı sorunlara sahiptir.

Promise uygulamaları standart bir davranışa uysa da genel API'leri farklıdır. JavaScript sözleri, RSVP.js'ye API açısından benzerdir. Söz oluşturmak için aşağıdaki adımları uygulayı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"));
  }
});

Promise oluşturucu, iki parametreli bir geri çağırma işlevi olan tek bir bağımsız değişken alır: resolve ve reject. Geri çağırma içinde bir işlem yapın (ör. eşzamansız), her şey yolunda giderse resolve'u, aksi takdirde reject'i çağırın.

Eski JavaScript'teki throw gibi, bir Error nesnesiyle reddetmek gelenekseldir ancak zorunlu değildir. Hata nesnelerinin avantajı, hata ayıklama araçlarını daha kullanışlı hale getiren bir yığın izi yakalamalarıdır.

Bu sözü nasıl kullanacağınız aşağıda açıklanmıştır:

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

then() iki bağımsız değişken alır: başarı durumu için bir geri çağırma ve başarısızlık durumu için başka bir geri çağırma. Her ikisi de isteğe bağlıdır. Bu nedenle, yalnızca başarı veya başarısızlık durumu için geri çağırma ekleyebilirsiniz.

JavaScript sözleri, DOM'da "Futures" olarak başladı, "Promises" olarak yeniden adlandırıldı ve sonunda JavaScript'e taşındı. Bu API'lerin DOM yerine JavaScript'te olması, Node.js gibi tarayıcı dışı JS bağlamlarında kullanılabilmeleri açısından çok iyi bir durumdur (çekirdek API'lerinde kullanılıp kullanılmadıkları ise ayrı bir sorudur).

DOM, JavaScript özelliği olmalarına rağmen bunları kullanmaktan çekinmez. Hatta, eşzamansız başarı/başarısızlık yöntemlerine sahip tüm yeni DOM API'leri sözleri kullanır. Bu durum, kota yönetimi, yazı tipi yükleme etkinlikleri, ServiceWorker, Web MIDI, Streams ve daha birçok özellik için geçerlidir.

Diğer kitaplıklarla uyumluluk

JavaScript promises API, then() yöntemi olan her şeyi promise benzeri (veya promise dilinde thenable sigh) olarak değerlendirir. Bu nedenle, Q promise döndüren bir kitaplık kullanıyorsanız bu sorun olmaz ve yeni JavaScript promises ile uyumlu çalışır.

Ancak, daha önce de belirttiğim gibi jQuery'nin Deferreds'leri biraz… faydasızdır. Neyse ki bunları standart sözlere dönüştürebilirsiniz. Bu işlemi en kısa sürede yapmanız önerilir:

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

Burada jQuery'nin $.ajax işlevi bir Deferred döndürür. then() yöntemi olduğundan Promise.resolve(), bunu JavaScript sözüne dönüştürebilir. Ancak bazen ertelenenler, 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 sözleri ise ilk hariç tümünü yok sayar:

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

Neyse ki bu genellikle istediğiniz şeydir veya en azından istediğiniz şeye erişmenizi sağlar. Ayrıca jQuery'nin, hata nesnelerini reddetme işlemlerine iletme kuralına uymadığını da unutmayın.

Karmaşık asenkron kod artık daha kolay

Haydi, biraz kod yazalım. Şunları yapmak istediğimizi varsayalım:

  1. Yükleme işlemini belirtmek için bir yükleme simgesi başlatın
  2. Bir hikaye için JSON getirerek başlığı ve her bölümün URL'lerini alıyoruz.
  3. Sayfaya başlık ekleme
  4. Her bölümü getirme
  5. Hikayeyi sayfaya ekleme
  6. Dönen çarkı durdurma

… ancak süreçte bir hata oluştuysa bunu da kullanıcıya bildirin. Bu noktada spinner'ı da durdurmak istiyoruz. Aksi takdirde spinner dönmeye devam eder, başı döner ve başka bir kullanıcı arayüzüne çarpar.

Elbette bir hikaye yayınlamak için JavaScript kullanmazsınız. HTML olarak yayınlamak daha hızlıdır. Ancak bu kalıp, API'lerle çalışırken oldukça yaygındır: Birden fazla veri getirme işlemi yapılır ve ardından tüm işlemler tamamlandığında bir işlem gerçekleştirilir.

Öncelikle ağdan veri getirme konusunu ele alalım:

XMLHttpRequest'i sözleştirme

Eski API'ler, geriye dönük uyumlu bir şekilde mümkünse sözleri kullanacak şekilde güncellenecektir. XMLHttpRequest bu iş için idealdir ancak bu arada GET isteği göndermek için 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 bu tekniği kullanalım:

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

Artık XMLHttpRequest yazmadan HTTP istekleri gönderebiliyoruz. Bu harika bir gelişme çünkü XMLHttpRequest'nın sinir bozucu camel-casing'ini ne kadar az görürsem hayatım o kadar mutlu olacak.

Zincirleme

then() ile her şey bitmez. Değerleri dönüştürmek veya ek eşzamansız işlemleri art arda çalıştırmak için then'leri zincirleyebilirsiniz.

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 şuna geri dönelim:

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

Yanıt JSON biçiminde ancak şu anda düz metin olarak alıyoruz. JSON'u kullanmak için get işlevimizi değiştirebiliriz responseType, ancak bunu sözler dünyasında da çö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ığından ve dönüştürülmüş bir değer döndürdüğünden, kısayol oluşturabiliriz:

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

Hatta getJSON() işlevini çok kolay bir şekilde oluşturabiliriz:

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

getJSON(), bir URL'yi getirip yanıtı JSON olarak ayrıştıran bir söz döndürmeye devam eder.

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

Ayrıca, then karakterlerini zincirleyerek asenkron işlemleri sırayla çalıştırabilirsiniz.

then() geri arama hizmetinden bir ürünü iade ettiğinizde sihirli bir deneyim yaşarsınız. Bir değer döndürürseniz sonraki then() bu değerle çağrılır. Ancak, söz benzeri bir şey döndürürseniz sonraki then() bunu bekler ve yalnızca söz yerine getirildiğ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 bulunuyoruz. Bu istek bize istenilecek bir URL grubu veriyor. Ardından, bu URL'lerden ilkini istiyoruz. Bu noktada, sözler basit geri çağırma kalıplarından gerçekten ayrışmaya başlar.

Hatta bölümleri almak için kısayol yöntemi 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);
})

story.json, getChapter çağrılana kadar indirilmez ancak getChapter sonraki çağrıldığında hikaye sözü yeniden kullanılır. Bu nedenle story.json yalnızca bir kez getirilir. Yay Promises!

Hata işleme

Daha önce gördüğümüz gibi, then() iki bağımsız değişken alır: biri başarı, diğeri ise hata (veya sözlerdeki anlamıyla yerine getirme ve reddetme) için kullanılır:

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

Ayrıca catch() aracını da kullanabilirsiniz:

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

catch() ile ilgili özel bir durum yok. Bu sadece then(undefined, func) için şeker görevi görüyor ancak daha okunabilir. Yukarıdaki iki kod örneğinin aynı şekilde davranmadığını, ikincisinin şuna eşdeğer olduğunu unutmayın:

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

Fark çok küçük olsa da son derece yararlıdır. Promise reddetmeleri, bir ret geri aramasıyla (veya eşdeğer olduğu için catch()) sonraki then()'ye atlar. then(func1, func2) ile func1 veya func2 çağrılır, ikisi birden çağrılmaz. Ancak then(func1).catch(func2) ile func1 reddederse her ikisi de zincirde ayrı adımlar olduğundan çağrılır. Aşağıdakileri yapı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 try/catch'e çok benzer. "try" içinde oluşan hatalar hemen catch() bloğuna gider. Yukarıdaki bilgileri akış şeması olarak da ekliyorum (çünkü akış şemalarını çok seviyorum):

Gerçekleşen vaatler için mavi çizgileri, reddedilenler için kırmızı çizgileri takip edin.

JavaScript istisnaları ve sözleri

Reddetme, bir söz açıkça reddedildiğinde gerçekleşir ancak oluşturucu geri çağırma işlevinde bir hata oluşursa örtülü olarak 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 nedenle, sözle ilgili tüm işlemlerinizi söz oluşturucu geri çağırma işlevi içinde yapmanız faydalıdır. Böylece hatalar otomatik olarak yakalanır ve reddedilir.

Aynı durum then() geri çağırmalarında oluşan 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);
})

Hata işlemeyi uygulamaya geçirme

Hikayemiz ve bölümlerimizle, kullanıcıya bir hata göstermek için catch ifadesini 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';
})

Getirme işlemi story.chapterUrls[0] başarısız olursa (ör. http 500 veya kullanıcı çevrimdışıysa), yanıtı JSON olarak ayrıştırmaya çalışan getJSON() içindeki dahil olmak üzere tüm sonraki başarılı geri çağırmalar atlanır ve chapter1.html'yi sayfaya ekleyen geri çağırma da atlanır. Bunun yerine catch geri çağırmasına geçer. Sonuç olarak, önceki işlemlerden herhangi biri başarısız olursa sayfaya "Bölüm gösterilemedi" mesajı eklenir.

JavaScript'teki try/catch gibi, hata yakalanır ve sonraki kod devam eder. Bu nedenle, istediğimiz gibi yükleme animasyonu her zaman gizlenir. Yukarıdaki kod, aşağıdaki kodun engellemeyen eşzamansız 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'

Hata kurtarma işlemi yapmadan catch() yalnızca günlük kaydı amacıyla kullanmak isteyebilirsiniz. Bunu yapmak için hatayı yeniden oluşturmanız yeterlidir. Bu işlemi getJSON() yöntemimizde yapabiliriz:

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

Bu nedenle, bir bölümü getirmeyi başardık ancak tüm bölümleri istiyoruz. Haydi başlayalım.

Paralellik ve sıralama: Her ikisinden de en iyi şekilde yararlanma

Asenkron düşünmek kolay değildir. Başlangıçta zorlanıyorsanız kodu senkronmuş 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 yarar. Ancak senkronize ediliyor ve indirme işlemi sırasında tarayıcı kilitleniyor. Bu işlemi asenkron hale getirmek için then() kullanılarak işlemlerin sırayla gerçekleşmesi sağlanır.

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';
})

Ancak bölüm URL'lerini nasıl döngüye alıp sırayla getirebiliriz? Bu çalışmaz:

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

forEach, eşzamansızlık konusunda bilgi sahibi olmadığı için bölümlerimiz, indirildikleri sırayla gösteriliyor. Bu da Pulp Fiction'ın yazılış şekline benziyor. Bu Pulp Fiction değil, o yüzden düzeltelim.

Sıra oluşturma

chapterUrls dizimizi bir dizi söz haline getirmek istiyoruz. Bunu then() 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);
  });
})

Bu, Promise.resolve() ile ilk karşılaşmamız. Bu ifade, kendisine verdiğiniz değerle sonuçlanan bir söz oluşturur. Promise örneği iletirseniz bu örnek olduğu gibi döndürülür (not: Bu, bazı uygulamaların henüz uymadığı spesifikasyonda yapılan bir değişikliktir). Bir söz gibi bir şey (then() yöntemi olan) iletirseniz aynı şekilde yerine getirilen/reddedilen gerçek bir Promise oluşturur. Başka bir değer iletirseniz (ör. Promise.resolve('Hello'), bu değerle karşılanan bir söz oluşturur. Yukarıdaki örnekte olduğu gibi değeri olmadan çağırırsanız "undefined" ile doldurulur.

Ayrıca, kendisine verdiğiniz değerle (veya tanımsız) reddeden bir söz oluşturan Promise.reject(val) da vardır.

Yukarıdaki kodu array.reduce kullanarak düzenleyebiliriz:

// 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ı işi yapar ancak ayrı bir "sequence" değişkeni gerektirmez. Dizideki her öğe için reduce geri çağırma işlevimiz çağrılır. "sequence" Promise.resolve() ilk seferde, ancak diğer görüşmelerde "sequence" önceki görüşmeden döndürdüğümüz değerdir. array.reduce bir diziyi tek bir değere indirgemek için çok kullanışlıdır. Bu örnekte bu değer bir sözdür.

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';
})

İşte bu kadar. Senkronizasyon sürümünün tamamen eş zamansız bir sürümünü oluşturduk. Ancak daha iyisini yapabiliriz. Şu anda sayfamız aşağıdaki gibi indiriliyor:

Tarayıcılar aynı anda birden fazla öğe indirme konusunda oldukça iyidir. Bu nedenle, bölümleri birbiri ardına indirerek performanstan ödün veriyoruz. Amacımız, hepsini aynı anda indirip tamamı geldiğinde işlemek. Neyse ki bu işlem için bir API var:

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

Promise.all, bir dizi söz alır ve tümü başarıyla tamamlandığında yerine getirilen bir söz oluşturur. Sonuçları, ilettiğiniz sözlerin sırasına göre alırsınız (hangi sözler yerine getirilirse getirilsin).

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 yöntem, tek tek yüklemeye kıyasla saniyeler içinde daha hızlı olabilir ve ilk denememize göre daha az kod içerir. Bölümler herhangi bir sırada indirilebilir ancak ekranda doğru sırada gösterilir.

Ancak algılanan performansı yine de iyileştirebiliriz. Birinci bölüm geldiğinde sayfaya eklemeliyiz. Bu sayede kullanıcı, diğer bölümler gelmeden okumaya başlayabilir. Üçüncü bölüm geldiğinde, kullanıcının ikinci bölümün eksik olduğunu fark etmeyebileceği için bu bölümü sayfaya eklemeyiz. İkinci bölüm geldiğinde ikinci ve üçüncü bölümleri ekleyebiliriz.

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

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 bu kadar. İki dünyanın en iyisi! Tüm içeriğin teslim edilmesi aynı süreyi alır ancak kullanıcı, içeriğin ilk kısmını daha erken alır.

Bu basit örnekte, tüm bölümler yaklaşık olarak aynı anda gelir ancak her seferinde bir bölüm göstermenin avantajı, daha fazla ve daha büyük bölümlerde daha belirgin olacaktır.

Yukarıdakileri Node.js tarzı geri çağırmalar veya etkinliklerle yapmak yaklaşık iki kat daha fazla kod gerektirir ancak daha da önemlisi, takip etmesi o kadar kolay değildir. Ancak bu, sözler için hikayenin sonu değildir. Diğer ES6 özellikleri ile birleştirildiğinde daha da kolaylaşırlar.

Bonus turu: genişletilmiş özellikler

Bu makaleyi ilk yazdığımdan beri, Promises'i kullanma olanağı büyük ölçüde genişledi. Chrome 55'ten beri eşzamansız işlevler, söz tabanlı kodun ana iş parçacığını engellemeden eşzamanlıymış gibi yazılmasına olanak tanır. Bu konu hakkında daha fazla bilgiyi asenkron işlevler hakkındaki makalemde bulabilirsiniz. Büyük tarayıcılarda hem Promises hem de async işlevleri yaygın olarak desteklenir. Ayrıntıları MDN'nin Promise ve async function referansında bulabilirsiniz.

Bu metni okuyup 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.

Ayrıca, makalenin çeşitli bölümlerini güncellediği için Mathias Bynens'e teşekkür ederiz.