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, kendinizi web geliştirme tarihindeki önemli bir ana hazırlayacaksınız.

[Trabbi başlıyor]

JavaScript'te vaatler geldi.

[Havai fişekler patlıyor, yukarıdan parıldayan kağıt yağmur yağıyor, kalabalık çılgınca]

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

  • İnsanlar etrafınızda tezahürat yapıyor ama telaşın ne olduğundan emin değilsiniz. Belki "vaat"in ne olduğundan bile emin değilsinizdir. Omuzlarını silkiyorsunuz ama parıltılı kağıt ağırlığı omuzlarınıza yük geliyor. Öyleyse merak etmeyin, bu konuyu neden önemsemem gerektiğini anlamam çok uzun zamanımı aldı. Bu işlemi en baştan yapmak isteyebilirsiniz.
  • Yumruklar havaya! Tam olarak vaktiniz var, değil mi? Bu Promise öğelerini daha önce kullanmıştınız, ancak tüm uygulamaların biraz farklı bir API'ye sahip olması sizi rahatsız ediyor. Resmi JavaScript sürümüne ait API nedir? Bu durumda muhtemelen terminolojiyle başlamak istersiniz.
  • Bunu zaten bilirsiniz ve zıplayıp duran kişilere yeni bir habermiş gibi güvenirsiniz. Bir dakikanızı ayırarak üstünlüğünüzü gösterin, ardından doğrudan API referansına gidin.

Tarayıcı desteği ve çoklu dolgu

Tarayıcı Desteği

  • 32
  • 12
  • 29
  • 8

Kaynak

Spesifikasyon uyumluluğu konusunda taahhütlerin tam olarak uygulanmadığı tarayıcıları getirmek veya diğer tarayıcılara ve Node.js'ye sözler eklemek için çoklu dolguya (2k gzip uygulanmış) bakın.

Peki ne dersiniz?

JavaScript tek iş parçacığıdır. Diğer bir deyişle, iki komut dosyası biti aynı anda çalışamaz; bunların birbiri ardına çalışması gerekir. Tarayıcılarda JavaScript, tarayıcıdan tarayıcıya farklılık gösteren bir dizi başka öğeyi içeren bir ileti dizisini paylaşır. Ancak genellikle JavaScript; boyama, stilleri güncelleme ve kullanıcı işlemlerini (ör. metni vurgulama ve form denetimleriyle etkileşimde bulunma) yönetme ile aynı sırada yer alır. Bunlardan birindeki etkinlik diğerlerini geciktirir.

İnsan olarak çok iş parçacıklısınız. Birden fazla parmağınızla yazabilir, bir diyaloğu aynı anda hem arabada hem de basılı tutabilirsiniz. Mücadele etmemiz gereken tek şey hapşırmadır. Bu durumda tüm mevcut faaliyetler hapşırma boyunca askıya alınır. Bu durum çok sinir bozucu. Özellikle araba sürerken konuşma yapmaya çalışırken. Hapşıran bir kod yazmak istemezsiniz.

Bu sorunu çözmek için büyük olasılıkla etkinlikleri ve geri çağırmaları kullandınız. 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ç hapşırık değil. Resmi alıp birkaç dinleyici ekleriz. Ardından JavaScript, bu işleyicilerden biri çağrılana kadar yürütmeyi durdurabilir.

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

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, hatalı olan görüntüleri biz daha dinleme fırsatı bulmadan yakalamıyor. Maalesef DOM bunun için bize bir yöntem sunmuyor. 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ı nesne (keyup, touchstart vb.) üzerinde birden çok kez gerçekleşebilecek şeyler için idealdir. Bu etkinliklerde, dinleyiciyi bağlamadan önce ne olduğuyla ilgilenmezsiniz. Ancak eş zamansız 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 şey bu, ama daha iyi adlandırma. 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
});

En basit haliyle, vaatler etkinlik işleyicilere benzerdir, sadece şunlar hariç:

  • Bir vaat yalnızca bir kez başarılı ya da başarısız olabilir. İki kez başarılı ya da başarısız olamaz. Ne başarılı ne de başarısızlığa geçemez.
  • Bir taahhüt başarılı veya başarısız olursa 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ş 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ği konusunda daha az ilginiz, sonuca daha çok tepki vermeniz beklenir.

Vaat terminolojisi

Domenic Denicola'nın kanıtı, bu makalenin ilk taslağını okudu ve terminoloji olarak beni "F" olarak derecelendirdi. 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 şöyle olabilir:

  • yerine getirildi - Taahhüdün başarıyla tamamlanmasıyla ilgili işlem
  • rejected (reddedildi): Vaatle ilgili işlem başarısız oldu
  • beklemede: Henüz yerine getirmedi veya reddetmedi
  • settled: Karşılandı 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ırlatıyor; bu nedenle onu mümkün olduğunca az kullanacağım.

Vaatler JavaScript'e gelir!

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

Yukarıdakiler ve JavaScript'in 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. Bununla birlikte, Ertelenenler Promise/A+ ile uyumlu olmadığından biraz farklı ve daha az faydalı olur. Bu nedenle dikkatli olun. jQuery, Promise türünde de olsa bulunur, ancak bu yalnızca Ertelenenler alt kümesidir ve aynı sorunlara sahiptir.

Vaat uygulamaları standartlaştırılmış bir davranış izler ancak genel API'leri birbirinden farklıdır. JavaScript vaatleri, API'deki LCV.js ile benzerdir. 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"));
  }
});

Vaadi oluşturucu tek bir bağımsız değişken ve iki parametreli bir geri çağırma alır, çözümle ve reddet. Geri çağırma sırasında eşzamansız gibi bir işlem yapın. Ardından, her şey yolundaysa çağrıyı sonlandırın, aksi halde isteği gönderin.

Düz eski JavaScript'teki throw gibi, bir hata nesnesiyle ret işlemi geleneksel olarak yapılı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ılı durum için bir geri çağırma ve başarısız durum için başka bir bağımsız değişken olmak üzere iki bağımsız değişken alır. Her ikisi de isteğe bağlıdır. Yani yalnızca başarılı veya başarısız durum için geri çağırma ekleyebilirsiniz.

JavaScript vaatleri, DOM'de "Vadeli" olarak başladı, "Promises" olarak yeniden adlandırıldı ve sonunda 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 bir JavaScript özelliği olsa da DOM, bunları kullanmaktan korkmaz. Aslında, eşzamansız başarı/başarısızlık yöntemlerine sahip tüm yeni DOM API'leri, vaat edilenleri kullanacaktı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 vaat ettikleriyle uyumlu olacaktır.

Bahsettiğim gibi, jQuery'nin Ertelenenler özelliği pek yardımcı olmadı. Neyse ki bunları standart vaatlere dönüştürebilirsiniz. Bunu da mümkün olan en kısa sürede yapabilirsiniz:

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) {
  // ...
})

Oysa JS, ilki hariç hepsinin göz ardı edileceğini vaat eder:

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 Hata nesnelerini retlere iletme kurallarına uymadığını da unutmayın.

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

Peki, şimdi bir şeyler kodlayalım. Şunları yapmak istediğimizi varsayalım:

  1. Yükleme durumunu göstermek için döner simgeyi başlatın
  2. Hikaye için bir JSON dosyası alın. Bu şekilde her bölümün başlığı ve URL'leri yer alır.
  3. Sayfaya başlık ekleyin
  4. Her bölümü getir
  5. Hikayeyi sayfaya ekleyin
  6. Döner simgeyi durdur

... ancak aynı zamanda kullanıcıya işlem sırasında bir sorun olup olmadığını da bildirir. Bu noktada da döner simgeyi durduracağız, aksi takdirde dönmeye devam edecek, baş dönmeye devam edecek ve başka bir kullanıcı arayüzüyle karşılaşacak.

HTML olarak sunmak daha hızlı olmakla birlikte bir hikaye sunmak için JavaScript'i kullanamazsınız. Ancak, API'lerle çalışırken bu düzen oldukça yaygındır: Birden fazla veri getirme, ardından işlem tamamlandığında bir işlem yapma.

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

XMLHttpRequest'i Vadetme

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 manuel olarak XMLHttpRequest yazmadan HTTP isteklerinde bulunabiliyoruz. Bu çok iyi. Çünkü XMLHttpRequest ürününün deve büyük/küçük harf kullanımını ne kadar az görmek zorunda kalırsam hayatım da o kadar mutlu olacak.

Zincirleme

then(), hikayenin sonu değil. Değerleri dönüştürmek veya ek eşzamansız işlemleri art arda çalıştırmak için then öğelerini birbirine zincirleyebilirsiniz.

Değerleri dönüştürme

Değerleri, yeni değeri döndürerek 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çimindedir ancak şu anda düz metin olarak alıyoruz. Get işlevimizi JSON responseType kullanacak şekilde değiştirebiliriz, ancak şu taahhütlerde bunu 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şkeni alıp 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 oldukça kolay hale getirebiliriz:

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

getJSON(), yine de bir URL döndüren ve yanıtı JSON olarak ayrıştıran bir söz döndürür.

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

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

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() bu değerle çağrılır. Bununla birlikte, vaat gibi bir öğeyi iade ederseniz bir sonraki then() bekler ve yalnızca bu vaat söz konusu olduğunda (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 istek göndeririz. Bu istek, bize istenecek bir URL grubu sağlar. Ardından, bunlardan ilkini isteriz. İşte bu noktada vaatler, basit geri çağırma kalıpları arasında öne çıkmaya başlar.

Bölümleri almak için kısayol da 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 bir sonraki sefer getChapter olarak adlandırılacak hikayeyi 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ı 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 davranışların aynı olmadığı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);
})

Aralarındaki fark küçük ama son derece yararlıdır. Taahhüt retleri, ret geri çağırması ile bir sonraki then()'ye (veya eşdeğer olduğu için catch()) atlanır. then(func1, func2) ile func1 veya func2 çağrılır; ikisi birden çağrılmaz. Ancak then(func1).catch(func2) ile bunlar zincirde ayrı adımlar olduğundan func1 reddedilirse 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 deneme-yakalama işlemine çok benzer. "Dene" içinde ortaya çıkan hatalar hemen catch() blokuna 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 taahhütleri

Bir vaat açıkça reddedildiğinde ancak oluşturucu geri çağırmasında dolaylı olarak hata atıldığında retler 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ı taahhüt oluşturucu geri çağırmanın içinde yapmanızın yararlı olacağı anlamına gelir. Böylece, hatalar otomatik olarak yakalanır ve reddedilir.

Aynı durum, then() geri çağırma işlevinde bildirilen 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 yakalamayı 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] öğesini getirme işlemi başarısız olursa (ör.http 500 veya kullanıcı çevrimdışıysa) sonraki tüm başarılı geri çağırmalar atlanır. getJSON() için yanıtı JSON olarak ayrıştırmaya çalışan bu geri çağırma da dahil olmak üzere, sayfaya chapter1.html ekleyen geri çağırma da 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 dene/yakala yöntemi gibi, hata yakalanır ve sonraki kod devam eder. Böylece, döner simge her zaman gizli kalır, biz de tam olarak bunu istiyoruz. Yukarıda, aşağıdakilerin engellemeyen 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, hatadan kurtarmadan sadece günlük kaydı amacıyla kullanabilirsiniz. Bunu yapmak için hatayı yeniden atın. 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 ama hepsini istiyoruz. Hadi ekleyelim.

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

Eş zamansız düşünmek kolay değildir. Doğru yolda ilerlemekte zorlanıyorsanız kodu sanki senkronize edilmiş 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'

Böyle olur. Ancak senkronizasyon çalışır ve öğeler indirilirken tarayıcı kilitlenir. Bu işi eşzamansız hale getirmek için then() öğelerini birbiri ardına gerçekleştirmek için kullanıyoruz.

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 nasıl dolaşabilir ve onları nasıl sıraya alabiliriz? Bu komut çalışamaz:

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 olabilir. Bu nedenle bölümlerimiz, indirildikleri sırayla gösterilir. Ucuz Kurgu filmi de bu şekilde yazılmıştır. Bu Pulp Kurgu değil, bu sorunu düzeltelim.

Dizi oluşturma

chapterUrls dizimizi bir vaatlere 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()'ı ilk kez görüyoruz. Bu, ona verdiğiniz değer ne olursa olsun çözüme ulaştırılacak bir söz oluşturuyor. 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öz konusu ifadeyi vaat gibi bir şekilde iletirseniz (then() yöntemi vardır) aynı şekilde karşılayan/reddeden gerçek bir Promise oluşturur. Örneğin, başka bir değer ilerlerseniz Promise.resolve('Hello') ise bu değerle gerçek bir vaat oluşturur. Yukarıda olduğu gibi değer olmadan çağırırsanız, "tanımsız" olarak yerine getirilir.

Bir de Promise.reject(val) vardır. Ona verdiğiniz (veya tanımlanmamış) değerle reddeden bir vaat oluşturur.

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 işlem, önceki örnekle aynı işlemi uygular ancak ayrı "sıra" değişkenine gerek yoktur. Dizideki her öğe için azaltılmış geri çağırma yöntemimiz çağrılır. "sequence" ilk seferde Promise.resolve() değerindedir, ancak "sequence"ın geri kalanı için önceki çağrıdan döndürdüğümüz ifadedir. array.reduce, bir diziyi tek bir değere indirmek için gerçekten faydalıdır. Bu örnekte oldukça iyi bir işlevdir.

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 versiyonunu ele aldık. Ama daha iyisini yapabiliriz. Şu anda sayfamız şu şekilde indiriliyor:

Tarayıcılar aynı anda birden fazla şeyi indirme konusunda oldukça iyidir. Bu nedenle, bölümleri peş peşe indirdiğimizde performansı kaybediyoruz. Yapmak istediğimiz şey, hepsini aynı anda indirmek ve hepsi geldiklerinde bunları işlemektir. Neyse ki bunun için bir API var:

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

Promise.all, bir dizi vaatte bulunur ve bunların tümü başarıyla tamamlandığında gerçekleşebilecek bir vaat oluşturur. Sözlerini yerine getirdiğiniz vaatlerle aynı sırayla bir dizi sonuç elde edersiniz.

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 istediğiniz sırada indirilebilir ancak ekranda doğru sırada görünürler.

Bununla birlikte, algılanan performansı iyileştirebiliriz. Birinci bölüm geldiğinde sayfaya eklemeliyiz. Böylece kullanıcı, bölümlerin geri kalanı 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 sayfayı sayfaya eklenmez. İkinci bölüm geldiğinde, ikinci ve üçüncü bölümleri ekleyebiliriz.

Bunu yapmak için tüm bölümlerimiz için JSON'u aynı anda alırız ve ardından 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';
})

Böylece her ikisinin de en iyisini yapabilirsiniz. Tüm içeriği yayınlamak aynı sürede aynı sürede sürer ancak kullanıcı ilk içeriği daha kısa sürede alır.

Bu basit örnekte, tüm bölümler yaklaşık olarak aynı saatte geliyor, ancak bölümleri teker teker göstermenin faydası, daha büyük bölümler olduğunda abartılı olacaktır.

Yukarıdakileri Node.js stili geri çağırmalar veya etkinliklerle yapmak, kodu iki katına çıkarır ancak daha önemlisi, takip etmek kolay değildir. Ancak bu sözlerin sonu değil. Diğer ES6 özellikleriyle birlikte kullanıldığında daha da kolaylaşıyorlar.

Bonus turu: genişletilmiş özellikler

Bu makaleyi ilk yazdığımdan bu yana Promises'i kullanma olanağı önemli ölçüde genişledi. Chrome 55'ten bu yana eşzamansız işlevler, ana iş parçacığını engellemeden eşzamanlı gibi vaat tabanlı kodun yazılmasına izin vermiştir. Bu konuyla ilgili daha fazla bilgiyi my async functions article bulabilirsiniz. Belli başlı tarayıcılarda hem Promises hem de eşzamansız işlevler için yaygın destek sunulmaktadır. Ayrıntıları MDN'nin Promise ve eş zamansız işlev referansında bulabilirsiniz.

Bunu gözden geçirip düzeltmeleri/önerileri 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'a da teşekkür ederiz.