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.

[Drumroll begins]

Promise'ler JavaScript'e geldi!

[Fireworks explode, glittery paper rains from above, the crowd goes wild]

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. Zamanında geldiniz. 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? Muhtemelen terminoloji ile başlamak istersiniz.
  • Bu durumu zaten biliyordunuz ve bu haberi yeni öğrenmiş 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ı kuyruktadı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şırıktır. Hapşırık 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:

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 tek bir resim yüklüyor. Bir resim grubunun ne zaman yüklendiğini bilmek istersek işler daha da karmaşık 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 şey istersiniz:

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 bir promise döndüren bir "ready" yöntemi olsaydı 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 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ı durumdan başarısıza veya tam tersi şekilde 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.

Bir şeyin tam olarak ne zaman kullanıma sunulduğundan ziyade sonuca tepki vermek istediğiniz için bu, ayarsız başarı/başarısızlık için son derece yararlıdır.

Söz terminolojisi

Domenic Denicola bu makalenin ilk taslağını gözden geçirdi ve terminoloji konusunda bana "F" notu verdi. Beni cezaya gönderdi, Eyaletler ve Kaderler'i 100 kez kopyalamaya zorladı ve ebeveynlerime endişeli bir mektup yazdı. Buna rağmen, terimlerle ilgili kafam karışmaya devam ediyor. Ancak temel bilgilere göz atın:

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 getirilmemiş veya reddedilmemiş
  • settled: Gerçekleştirildi veya reddedildi

Spesifikasyon, then yöntemine sahip olması nedeniyle söze benzeyen bir nesneyi tanımlamak için thenable terimini de kullanı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ıdakiler ve JavaScript taahhüdü, Promises/A+ adlı ortak ve standartlaştırılmış bir davranışı paylaşır. jQuery kullanıcısıysanız Deferreds adlı benzer bir öğeye sahiptirler. 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. Söz vermeyi aşağıdaki şekilde yapabilirsiniz:

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 izleme yakalamaları sayesinde hata ayıklama araçlarını daha faydalı hale getirmeleridir.

Bu güvenceyi 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(), 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 umutları, DOM'da "Futures" olarak başladı, "Promises" olarak yeniden adlandırıldı ve sonunda JavaScript'e taşındı. Bu özelliklerin DOM yerine JavaScript'te bulunması çok iyi bir gelişme. Çünkü bu özellikler Node.js gibi tarayıcı dışı JS bağlamlarında kullanılabilir (temel API'lerinde bu özelliklerden yararlanıp yararlanmayacakları ise başka bir soru).

Bunlar JavaScript özelliği olmasına rağmen 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. Bu, Kota Yönetimi, Yazı Tipi Yükleme Etkinlikleri, ServiceWorker, Web MIDI, Akışlar ve daha birçok özellikte zaten uygulanıyor.

Diğer kitaplıklarla uyumluluk

JavaScript promises API, then() yöntemi içeren her şeyi promise benzeri (veya promise dilinde thenable oh) olarak değerlendirir. Bu nedenle, Q promise döndüren bir kitaplık kullanıyorsanız sorun olmaz. Bu kitaplık, yeni JavaScript promises ile iyi çalışı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 bir Deferred döndürür. then() yöntemi olduğundan Promise.resolve(), bunu JavaScript vaadine dönüştürebilir. Ancak bazen ertelenenler geri çağırma işlevlerine birden fazla bağımsız değişken iletebilir. Ö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ğiniz şeye 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

Tamam, kod yazalım. Aşağıdakileri yapmak istediğimizi varsayalım:

  1. Yüklemeyi belirtmek için bir döner simge başlatma
  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 ekleme
  4. Her bölümü getirme
  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 yayınlamak 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 uyumlu bir şekilde mümkünse söz vermeyi kullanacak şekilde güncellenecektir. XMLHttpRequest birincil adaydır ancak bu sırada 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 modeli 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 ayarsız işlemler çalıştırmak 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);
})

Aslında getJSON() işlevini çok kolay bir şekilde oluşturabiliriz:

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 çağırma işlevinden bir öğeyi iade ettiğinizde bu işlem biraz büyülü olur. 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 dizi URL verir. Ardından bu URL'lerden ilkini isteriz. Bu noktada, umutlar basit geri çağırma kalıplarından gerçekten öne çıkmaya başlar.

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 indirilmez ancak getChapter çağrıldıktan sonra hikaye vaadini yeniden kullanırız. Bu nedenle story.json yalnızca bir kez getirilir. Yaşasın Promises!

Hata işleme

Daha önce de gördüğümüz gibi, then() bir başarı, bir de başarısızlık (veya söz verme dilinde yerine getirme ve reddetme) 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() simgesini de kullanabilirsiniz:

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

catch(), then(undefined, func) için şeker gibidir ancak daha okunaklıdır. 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. Söz verme reddi, bir ret geri çağırma işlevi (veya eşdeğeri olduğu için catch()) ile sonraki then()'e atlar. 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ıdakileri akış şeması olarak görebilirsiniz (akış şemalarını seviyorum):

Yerine getirilen sözler için mavi çizgileri, reddedilen sözler için kırmızı çizgileri takip edin.

JavaScript istisnaları ve umutları

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, catch geri çağırma işlevine 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ı oluşturmak 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. Bunu yapalım.

Paralellik ve sıralama: Her ikisinden 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şlemleri 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 Fiction değil, bu yüzden sorunu düzeltelim.

Sıralamayı oluşturma

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

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. Promise örneği gönderirseniz işlev bunu döndürür (not: bu, bazı uygulamaların henüz uymadığı 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 karşılanan bir söz oluşturur. Yukarıdaki gibi değer vermeden çağırırsanız "undefined" ile doldurulur.

Ayrıca, ona verdiğiniz değerle (veya tanımlanmamış) reddeden bir söz veren Promise.reject(val) de vardır.

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 kullanışlı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 yöntem, tek tek yüklemekten saniyeler daha hızlı olabilir ve ilk denememize kıyasla daha az kod içerir. 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 getirip bunları dokümana eklemek üzere 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, her ikisinin de en iyisi. Tüm içeriğin yayınlanması aynı süreyi alır 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 özellikler ES6'daki diğer özelliklerle birlikte kullanıldığında 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 beri, eşzamansız işlevler, promise tabanlı kodun eşzamanlıymış gibi yazılmasına ancak ana iş parçacığı engellenmeden yazılmasına olanak tanıdı. 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.