JavaScript Vaatleri: giriş

Sözler, ertelenen 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 tarihindeki önemli bir ana hazırlanın.

[Trabbi başlıyor]

JavaScript'te vaatler geldi.

[Havai fişekler patlıyor, yukarıdan ışıltılı kağıtlar yağıyor, kalabalık çılgına dönüyor]

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

  • Etrafınızdaki insanlar tezahürat yapıyor ancak neden bu kadar heyecanlı olduklarını bilmiyorsunuz. "Söz"ün ne olduğundan bile emin olmayabilirsiniz. Omuz silkersiniz ama parıltılı kağıdın ağırlığı omuzlarınıza çöker. Cevabınız evetse endişelenmeyin. Bu konularla neden ilgilenmem gerektiğini anlamak benim de epey zamanımı aldı. Muhtemelen baştan başlamak istersiniz.
  • Havaya yumruk atıyorsunuz. Tam olarak vaktiniz var, değil mi? Bu Promise öğelerini daha önce kullanmış olsanız da tüm uygulamaların API'sinin biraz farklı olması sizi rahatsız ediyor. Resmi JavaScript sürümü için API nedir? Bu durumda muhtemelen terminolojiyle başlamak istersiniz.
  • Bu durumu zaten biliyordunuz ve bu haberi ilk kez duymuş gibi heyecanlananlara gülüyorsunuz. Bir süreliğine üstünlüğünüzün keyfini çıkarın ve ardından doğrudan API referansı'na gidin.

Tarayıcı desteği ve polyfill

Tarayıcı desteği

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

Kaynak

Tam bir promises uygulaması olmayan tarayıcıları spesifikasyona uygun hale getirmek veya diğer tarayıcılara ve Node.js'e promises eklemek için polyfill'e (2k sıkıştırılmış) göz atın.

Peki Instagram neden bu kadar popüler?

JavaScript tek iş parçacıklı olduğundan iki komut dosyası aynı anda çalışamaz. Komut dosyalarının birbiri ardına çalışması gerekir. Tarayıcılarda JavaScript, tarayıcıdan tarayıcıya değişen birçok başka öğe içeren bir ileti dizisi paylaşır. Ancak JavaScript genellikle boyama, stilleri güncelleme ve kullanıcı işlemlerini (ör. metni vurgulama ve form denetimleriyle etkileşim kurma) işleme ile aynı sıradadır. Bu öğelerden birinde etkinlik olduğunda diğerleri gecikir.

İnsan olarak çok iş parçacıklısınız. Birden fazla parmağınızla yazabilir, araba kullanırken aynı anda sohbet edebilirsiniz. Karşılaştığımız tek engelleme işlevi hapşırmadır. Hapşırma sırasında tüm mevcut etkinliklerin askıya alınması gerekir. Bu durum, özellikle araba kullanırken sohbet etmeye çalıştığınızda oldukça can sıkıcı olabilir. Sorunlu kod yazmak istemezsiniz.

Bu sorunun üstesinden gelmek için muhtemelen etkinlikleri ve geri aramaları kullanmışsınızdır. Etkinlikler şunlardır:

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

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

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

Bu hiç de saçma değil. Resmi alırız, birkaç dinleyici ekleriz. Ardından JavaScript, bu dinleyicilerden biri çağrılana kadar yürütülmeyi durdurabilir.

Maalesef yukarıdaki örnekte, etkinlikleri dinlemeye başlamadan önce gerçekleşmiş olabilir. Bu nedenle, resimlerin "tamamlandı" ö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 yöntem, henüz dinleme fırsatı bulamadan hata veren resimleri yakalamaz. DOM maalesef bunu yapmamıza olanak tanımaz. Ayrıca, bu işlem bir resim yükler. Bir dizi resmin ne zaman yüklendiğini öğrenmek istersek işler daha da karmaşık bir hale gelir.

Etkinlikler her zaman en iyi yöntem değildir

Etkinlikler, aynı nesnede birden çok kez gerçekleşebilecek işlemler (keyup, touchstart vb.) için mükemmeldir. Bu etkinliklerde, dinleyiciyi eklemeden önce ne olduğuna dikkat etmeniz gerekmez. Ancak ayarsız başarı/başarısızlık söz konusu olduğunda ideal olarak aşağıdaki gibi bir durumla karşılaşmanız gerekir:

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

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

Sözler de bunu yapar ancak daha iyi bir adlandırmayla. HTML resim öğelerinde vaat döndüren "hazır" bir yöntem varsa bunu yapabilirdik:

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

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

Temel olarak, umutlar etkinlik işleyicilere biraz benzer. Bununla birlikte, umutlar aşağıdakiler dışında etkinlik işleyicilere benzemez:

  • Bir vaat yalnızca bir kez başarılı ya da başarısız olabilir. İki kez başarılı veya başarısız olamaz, başarılı olmaktan başarısıza veya başarısızdan başarıya geçemez.
  • Bir promise 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, eş zamanlı olmayan başarı/başarısızlık için son derece faydalıdır. Çünkü bir şeyin tam olarak ne zaman kullanılabilir hale geldiğiyle daha az ilgilenmeniz ve sonuca daha çok tepki vermeniz gerekir.

Söz terminolojisi

Domenic Denicola bu makalenin ilk taslağını gözden geçirdi ve terminoloji için bana "F" notu verdi. Beni gözaltına aldı, Eyaletler ve Kafatlar'ı 100 kez okumaya zorladı ve ve babama kaygılı bir mektup yazdı. Buna rağmen hâlâ birçok terminolojiyi karıştırıyorum ancak temel bazı ilkeler şunlar:

Sözler şunlar olabilir:

  • fulfilled: Sözle ilgili işlem başarılı oldu
  • reddedildi: Taahhütle ilgili işlem başarısız oldu
  • beklemede: Henüz yerine getirmedi veya reddetmedi
  • settled: Gerçekleştirildi veya reddedildi

Spesifikasyonda, then yöntemine sahip olması açısından vaat benzeri olan bir nesneyi tanımlamak için thenable terimi de kullanılmıştır. Bu terim bana eski İngiltere futbol menajeri Terry Venables'ı hatırlattığı için mümkün olduğunca az kullanacağım.

JavaScript'de Promise'ler kullanıma sunuldu

Sözler, kitaplık biçiminde bir süredir kullanılmaktadır. Örneğin:

Yukarıdaki işlemler ve JavaScript vaatleri, Promises/A+ adı verilen yaygın, standartlaştırılmış bir davranışı paylaşmaktadır. jQuery kullanıcısıysanız, bunlar Ertelenenler adlı benzer bir özelliğe sahiptir. Ancak ertelenenler Promise/A+ uyumlu değildir. Bu nedenle, farklı ve daha az kullanışlı olduklarından dikkatli olun. jQuery'de Promise türü de vardır ancak bu, ertelenenlerin yalnızca bir alt kümesidir ve aynı sorunlara sahiptir.

Söz uygulamalarında standart bir davranış izlense de genel API'leri farklıdır. JavaScript taahhüdü, API'de RSVP.js'ye benzer. Vaat oluşturmak için şu 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, resolve ve reject olmak üzere iki parametre içeren bir geri çağırma işlevi olan bir bağımsız değişken alır. Geri çağırma işlevinde bir şey yapın (belki de asynkron olarak), ardından her şey yolunda giderse resolve işlevini, aksi takdirde reject işlevini çağırın.

Eski JavaScript'teki throw gibi, bir Error nesnesi ile reddetmek yaygın bir uygulamadır ancak zorunlu değildir. Hata nesnelerinin avantajı, yığın izlemeyi yakalamaları ve hata ayıklama araçlarını daha faydalı hale getirmeleridir.

İşte bu sözü nasıl kullanacağınız:

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

then(), başarı durumu için bir geri çağırma ve başarısızlık durumu 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. Bu nedenle, yalnızca başarı veya başarısızlık durumu için geri çağırma işlevi ekleyebilirsiniz.

JavaScript vaatleri, DOM'de "Vadeli" olarak başladı, "Promises" olarak yeniden adlandırıldı ve son olarak JavaScript'e taşındı. Bunların DOM yerine JavaScript'te tutulması harikadır, çünkü Node.js gibi tarayıcı dışı JS bağlamlarında kullanılabilirler (başka bir soru da bunları temel API'lerinde kullanıp kullanmamalarıdır).

Bunlar JavaScript özelliği olsa da DOM bunları kullanmaktan çekinmez. Aslında, eşzamansız başarı/başarısızlık yöntemlerine sahip tüm yeni DOM API'leri umutları kullanır. Kota Yönetimi, Yazı Tipi Yükleme Etkinlikleri, ServiceWorker, Web MIDI, Akışlar ve daha fazlası bu alanda zaten yürürlükte.

Diğer kitaplıklarla uyumluluk

JavaScript, API'nin then() yöntemini içeren her şeyi vaat benzeri (veya thenable sözünü söyleyerek iç çek) olarak ele alacağını taahhüt eder. Bu nedenle, Q sözü döndüren bir kitaplık kullanırsanız sorun değil, yeni JavaScript'in vaatleri ile birlikte düzgün bir şekilde davranır.

Ancak, daha önce de belirttiğim gibi, jQuery'nin ertelenen işlemleri biraz ... faydalı değil. Neyse ki bunları standart vaatlere dönüştürebilirsiniz. Bunu en kısa sürede yapmanız önerilir:

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

Burada, jQuery'nin $.ajax işlevi Ertelenenli değerini döndürür. then() yöntemi olduğu için Promise.resolve() bunu bir JavaScript taahhüdüne dönüştürebilir. Ancak bazen ertelenmişler, geri çağırma işlemlerine 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 ilk öğe hariç tümünü yok sayar:

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

Neyse ki bu genellikle istediğiniz şeydir veya en azından istediğinize erişmenizi sağlar. Ayrıca jQuery'nin, reddedilen öğelere Error nesneleri gönderme kuralına uymadığını unutmayın.

Karmaşık ayarsız kodlar artık daha kolay

Peki, şimdi bir şeyler kodlayalım. Aşağıdakileri yapmak istediğimizi varsayalım:

  1. Yükleme durumunu göstermek için döner simgeyi başlatın
  2. Bir hikaye için JSON'u getiriyoruz. Bu JSON, bize hikayenin başlığını ve her bölümün URL'sini verir.
  3. Sayfaya başlık ekleyin
  4. Her bölümü getir
  5. Hikayeyi sayfaya ekleme
  6. Dönen çubuğu durdurma

… ancak bu süreçte bir sorun oluşursa kullanıcıya da bildirin. Bu noktada döndürücüyü de durdurmak isteriz. Aksi takdirde döndürücü dönmeye devam eder, baş dönmesi yaşar ve başka bir kullanıcı arayüzüne çarpar.

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

Öncelikle ağdan veri getirmeyle ilgilenelim:

XMLHttpRequest'i Promise'e dönüştürme

Eski API'ler, geriye dönük olarak uyumlu bir şekilde mümkünse vaatleri kullanacak şekilde güncellenecektir. XMLHttpRequest önemli bir adaydır, ancak bu arada GET isteği yapmak 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 bunu kullanalım:

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

Artık XMLHttpRequest'ü manuel olarak yazmadan HTTP istekleri gönderebiliyoruz. Bu harika bir gelişme. Çünkü XMLHttpRequest'ün can sıkıcı camel-casing'ini ne kadar az görürsem o kadar mutlu olurum.

Zincirleme

then(), hikayenin sonu değildir. Değerleri dönüştürmek veya art arda ek asynkron işlemler yürütmek için then'leri birbirine bağlayabilirsiniz.

Değerleri dönüştürme

Değerleri dönüştürmek için yeni değeri döndürmeniz yeterlidir:

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

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

Yanıt JSON biçimindedir ancak şu anda düz metin olarak alıyoruz. get işlevimizi JSON responseType kullanacak şekilde değiştirebiliriz ancak bunu promises 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 alır ve dönüştürülmüş bir değer döndürür. Bu nedenle kısayol kullanabiliriz:

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

Hatta getJSON() işlevini oldukça kolay hale getirebiliriz:

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

getJSON(), bir URL'yi alan ve ardından yanıtı JSON olarak ayrıştıran bir promise döndürmeye devam eder.

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

Asynkron işlemleri sırayla çalıştırmak için then'leri de zincirleyebilirsiniz.

then() geri aramasından bir şey döndürdüğünüzde kendinizi sihirli bir şekilde hissedersiniz. Bir değer döndürürseniz sonraki then() işlevi bu değerle çağrılır. Ancak, söze benzer bir şey döndürürseniz sonraki then() bunu bekler ve yalnızca söz sona erdiğinde (başarılı/başarısız olduğunda) ç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 bir eşzamansız istek göndeririz. Bu istek bize isteyecek bir URL grubu verir. Ardından bu URL'lerden ilkini isteriz. Bu noktada, basit geri çağırma kalıplarından farklı olarak asıl farkını gösterenler söz konusu işlevlerdir.

Bölümlere gitmek için kısayol yöntemi de 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 dosyasını indirmeyiz ancak getChapter bir sonraki seferin adı hikaye olarak tekrar kullanılır, bu nedenle story.json yalnızca bir kez getirilir. Yaşasın Promises!

Hata işleme

Daha önce gördüğümüz gibi, then(), biri başarı için, diğeri başarısızlık (veya vaatlerde yapılan konuşmada yerine getir ve reddet) 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);
})

catch() uzantısı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 şey yok. then(undefined, func) için çok şeker ama daha okunabilir. Yukarıdaki iki kod örneğinin aynı şekilde davranmadığını unutmayın. İkinci örnek şuna eşdeğerdir:

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

Bu fark çok küçük olsa da son derece faydalıdır. Taahhüt retleri, ret geri çağırması ile bir sonraki then()'ye (veya eşdeğer olduğundan catch()) atlanır. then(func1, func2) ile func1 veya func2 çağrılır, ikisinin birden çağrılmasına izin verilmez. Ancak then(func1).catch(func2) ile, zincirdeki ayrı adımlar oldukları için func1 reddederse her ikisi de ç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 gerçekleşen hatalar hemen catch() bloğuna gider. Yukarıda bir akış şeması olarak yukarıyı görebilirsiniz (çünkü akış şemalarını çok seviyorum):

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

JavaScript istisnaları ve vaatleri

Reddetmeler, bir söz açıkça reddedildiğinde gerçekleşir ancak yapıcı geri çağırma işlevinde bir hata atıldığında da dolaylı olarak 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, promise ile ilgili tüm çalışmalarınızı promise 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şturulan 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

Hikayemiz ve bölümlerimizle, kullanıcıya hata göstermek için catch 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 işlemi başarısız olursa (ör. http 500 hatası veya kullanıcı çevrimdışıysa) yanıtı JSON olarak ayrıştırmaya çalışan getJSON() içindeki ve sayfaya chapter1.html dosyasını ekleyen geri çağırma işlevi de dahil olmak üzere sonraki tüm başarı geri çağırma işlevleri atlanır. Bunun yerine, yakalama geri çağırmasına geçer. Sonuç olarak, önceki işlemlerden herhangi biri başarısız olursa sayfaya "Bölüm gösterilemedi" ifadesi eklenir.

JavaScript'in try/catch işlevi gibi, hata yakalanır ve sonraki kod devam eder. Böylece, isteğimiz gibi döndürme çubuğu her zaman gizli kalır. Yukarıdaki kod, aşağıdaki kodun engellenmeyen bir 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'

Hatayı düzeltmeden yalnızca günlük kaydı için catch()'ü kullanabilirsiniz. Bunun için hatayı yeniden göndermeniz yeterlidir. Bunu 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;
  });
}

Bir bölümü getirmeyi başardık ancak hepsini istiyoruz. Hadi ekleyelim.

Paralellik ve sıralama: İkisinden de en iyi şekilde yararlanma

Asenkron düşünmek kolay değildir. Hedefe ulaşmakta zorlanıyorsanız kodu senkronizeymiş 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 yöntem kesinlikle işe yarıyor. Ancak senkronizasyon yapıyor ve indirme işlemi sırasında tarayıcıyı kilitliyor. Bu işlemin asynkron olarak yapılmasını sağlamak için işlemleri birbiri ardına yapmak üzere then() kullanırız.

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? Aşağıdakiler 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şzamanlı olmayan işlemleri desteklemediğinden, bölümlerimiz indirildikleri sırada gösterilir. Bu, Pulp Fiction'un yazılma şeklidir. Bu, Pulp Kurgu değil, bu sorunu düzeltelim.

Sıralamayı oluşturma

chapterUrls dizimizi bir söz dizisine dönüştürmek istiyoruz. Bu işlemi 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);
  });
})

Promise.resolve() değerini ilk kez görüyoruz. Bu değer, ona verdiğiniz değere göre çözülen bir söz oluşturur. Bir Promise örneğini iletirseniz yalnızca döndürülür (not: Bu, bazı uygulamaların henüz kullanmadığı spesifikasyonda yapılan bir değişikliktir). Söze benzer bir şey (then() yöntemi olan) iletirse aynı şekilde yerine getiren/reddeden gerçek bir Promise oluşturur. Başka bir değer gönderirseniz (ör. Promise.resolve('Hello') ise bu değerle gerçek bir vaat oluşturur. Yukarıdaki gibi değer vermeden çağırırsanız "undefined" ile doldurulur.

Bir de Promise.reject(val), ona verdiğiniz (veya tanımlanmamış) değerle reddeden bir vaat oluşturan bir terimdir.

array.reduce kullanarak yukarıdaki kodu 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 örnek, önceki örnekle aynı işlemi yapar ancak ayrı bir "dize" değişkenine ihtiyaç duymaz. reduce geri çağırma işlevimiz, dizideki her öğe için çağrılır. "sequence", ilk çağrıda Promise.resolve() olur ancak çağrıların geri kalanında "sequence", önceki çağrıdan döndürdüğümüz değerdir. array.reduce, bir diziyi tek bir değere indirgemek için son derece faydalıdır. Bu durumda söz konusu değer bir promise'dir.

Tüm bunları 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 senkronizasyon sürümünün tamamen eş zamansız bir sürümü. Ancak daha iyisini yapabiliriz. Sayfamız şu anda aşağıdaki gibi indiriliyor:

Tarayıcılar birden fazla şeyi aynı anda indirme konusunda oldukça başarılıdır. Bu nedenle, bölümleri birbiri ardına indirerek performans kaybediyoruz. Hepsini aynı anda indirip hepsi geldiğinde işlemek istiyoruz. Neyse ki bunun için bir API var:

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

Promise.all, bir dizi söz alır ve bunların tümü başarıyla tamamlandığında yerine getirilen bir söz oluşturur. Gönderdiğiniz sözlerle aynı sırayla bir dizi sonuç (sözlerin karşılandığı değerler) alırsınız.

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 göre saniyeler daha hızlı olabilir ve ilk denememizden daha az kod sunar. Bölümler herhangi bir sırada indirilebilir ancak ekranda doğru sırada gösterilir.

Bununla birlikte, algılanan performansı iyileştirebiliriz. Birinci bölüm geldiğinde sayfaya ekleyeceğiz. Bu sayede kullanıcı, diğer bölümler gelmeden önce okumaya başlayabilir. Üçüncü bölüm geldiğinde, kullanıcı 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 getirir ve ardından bunları dokümana 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';
})

Böylece her ikisinin de en iyisini yapabilirsiniz. Tüm içeriğin yayınlanması için aynı süre gerekir ancak kullanıcı ilk içeriği daha erken alır.

Bu basit örnekte, tüm bölümler yaklaşık olarak aynı anda yayınlanıyor ancak daha fazla ve daha büyük bölümler olduğunda birer birer göstermenin avantajı daha da artar.

Yukarıdakileri Node.js tarzı geri çağırma veya etkinliklerle yapmak, kodun yaklaşık iki katı kadar yer kaplar ancak daha da önemlisi, takip edilmesi o kadar kolay değildir. Ancak, söz konusu işlevler ES6'daki diğer özelliklerle birlikte kullanıldığında söz konusu işlevler daha da kolay hale gelir.

Bonus turu: Genişletilmiş özellikler

Bu makaleyi ilk kez yazdığımdan bu yana, Promise'leri kullanma olanağı büyük ölçüde genişledi. Chrome 55'ten bu yana eşzamansız işlevler, ana iş parçacığını engellemeden eşzamanlı olarak vaat tabanlı kodun yazılmasına izin vermiştir. Bu konu hakkında daha fazla bilgiyi asynchronize işlevler makalemde bulabilirsiniz. Ana tarayıcılarda hem Promise'ler hem de asynkron işlevler için yaygın destek vardır. Ayrıntıları MDN'nin Promise ve async işlevi referansında bulabilirsiniz.

Bu makaleyi 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.

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