Promise'ler, ertelenmiş ve eşzamansız hesaplamaları basitleştirir. Promise, henüz tamamlanmamış bir işlemi temsil eder.
Geliştiriciler, web geliştirme tarihinde önemli bir ana hazırlanın.
[Trampet çalmaya başlar]
JavaScript'te Promise'ler kullanıma sunuldu.
[Havai fişekler patlar, yukarıdan simli kağıtlar yağar, kalabalık coşar]
Bu noktada aşağıdaki kategorilerden birine girersiniz:
- Etrafınızdaki insanlar tezahürat yapıyor ancak bu heyecanın nedenini bilmiyorsunuz. "Söz"ün ne olduğundan bile emin değilsiniz. Omuz silkiyorsunuz ama parlak kağıdın ağırlığı omuzlarınızı aşağı çekiyor. Öyleyse endişelenmeyin, bu konuları önemsemem gerektiğini anlamam benim de çok zamanımı aldı. Muhtemelen baştan başlamak istersiniz.
- Havayı yumrukluyorsunuz. Zamanı gelmişti, değil mi? Daha önce Promise'leri 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? İşe terminoloji ile başlamak isteyebilirsiniz.
- Bunu zaten biliyordunuz ve yeni bir şeymiş gibi heyecanlananlarla dalga geçiyorsunuz. Üstünlüğünüzün tadını çıkarın, ardından doğrudan API referansına gidin.
Tarayıcı desteği ve polyfill
Eksiksiz bir promise uygulaması olmayan tarayıcıları spesifikasyon uyumluluğuna getirmek veya diğer tarayıcılara ve Node.js'ye promise eklemek için polyfill'e (2k gzipped) göz atın.
Bu yaygara neden?
JavaScript tek iş parçacıklıdır. Yani iki komut dosyası aynı anda çalıştırılamaz, birbiri ardına çalıştırılmaları gerekir. Tarayıcılarda JavaScript, tarayıcıdan tarayıcıya değişen birçok başka şeyle bir iş parçacığını paylaşır. Ancak JavaScript genellikle boyama, stilleri güncelleme ve kullanıcı işlemlerini (ör. metni vurgulama ve form kontrolleriyle etkileşim kurma) işleme ile aynı sıradadır. Bunlardan birindeki etkinlik diğerlerini geciktirir.
Bir insan olarak, çoklu iş parçacığına sahipsiniz. Birden fazla parmağınızla yazabilir, araba kullanırken aynı anda sohbet edebilirsiniz. Uğraşmamız gereken tek engelleyici işlev, hapşırmadır. Hapşırma sırasında mevcut tüm etkinlikler askıya alınmalıdır. Bu durum, özellikle araç kullanırken ve konuşmaya çalışırken oldukça can sıkıcı olabilir. Hapşıran bir kod yazmak istemezsiniz.
Bu sorunu çözmek için muhtemelen etkinlikleri ve geri çağırmaları 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 hapşırtıcı değil. Resmi alır, birkaç dinleyici ekleriz. Ardından JavaScript, bu dinleyicilerden biri çağrılana kadar yürütmeyi durdurabilir.
Maalesef yukarıdaki örnekte, etkinlikler biz onları 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, dinleme şansımız olmadan önce hata veren resimleri yakalamaz; maalesef DOM bunu yapmamıza izin vermez. Ayrıca bu, bir resim yüklüyor. Bir grup resmin ne zaman yüklendiğini öğrenmek istediğimizde işler daha da karmaşık hale gelir.
Etkinlikler her zaman en iyi yöntem olmayabilir.
Etkinlikler, aynı nesnede birden fazla kez gerçekleşebilecek şeyler için idealdir (ör. 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 şö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
});
Bu, vaatlerin yaptığı şeydir ancak daha iyi bir adlandırmayla yapılır. HTML resim öğelerinde, bir söz döndüren "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
});
En temel düzeyde, Promise'ler etkinlik işleyicilere benzer ancak şu farklar vardır:
- Bir promise 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ıyken başarısızlığa ya da başarısızken başarıya geçemez.
- Bir vaat başarılı veya başarısız olursa 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ş zamansız başarı/başarısızlık durumları için son derece kullanışlıdır. Çünkü bir şeyin tam olarak ne zaman kullanılabilir hale geldiğiyle daha az, sonuca tepki vermekle daha çok ilgilenirsiniz.
Promise terminolojisi
Domenic Denicola bu makalenin ilk taslağını okudu ve terminoloji için bana "F" verdi. Beni cezaya bıraktı, States and Fates'i 100 kez kopyalamaya zorladı ve aileme endişeli bir mektup yazdı. Buna rağmen, terimlerin çoğunu hâlâ karıştırıyorum ama temel bilgiler şunlar:
Sözler şunlar olabilir:
- fulfilled (yerine getirildi): Promise ile ilgili işlem başarılı oldu.
- rejected: Promise ile ilgili işlem başarısız oldu.
- Beklemede: Henüz karşılanmadı veya reddedilmedi.
- settled: Yerine getirildi veya reddedildi
Spesifikasyon, then yöntemine sahip olması nedeniyle Promise benzeri bir nesneyi tanımlamak için thenable terimini de kullanır. Bu terim bana İngiltere futbol takımının eski menajeri Terry Venables'ı hatırlatıyor. Bu nedenle, mümkün olduğunca az kullanacağım.
JavaScript'te Promise'ler kullanıma sunuldu.
Promise'ler bir süredir kitaplıklar biçiminde kullanılmaktadır. Örneğin:
Yukarıdaki ve JavaScript Promise'leri, Promises/A+ adı verilen ortak ve standartlaştırılmış bir davranışı paylaşır. jQuery kullanıcısıysanız Deferreds adında benzer bir özellik vardır. Ancak Deferred'ler Promise/A+ uyumlu değildir. Bu da onları biraz farklı ve daha az kullanışlı hale getirir. Bu nedenle dikkatli olun. jQuery'de Promise türü de vardır ancak bu yalnızca Deferred'in bir alt kümesidir ve aynı sorunlara sahiptir.
Promise uygulamaları standart bir davranışı izlese de genel API'leri farklıdır. JavaScript vaatleri, API açısından RSVP.js'ye benzer. 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, bir bağımsız değişken alır. Bu bağımsız değişken, resolve ve reject olmak üzere iki parametre içeren bir geri çağırmadır. Geri çağırma içinde bir işlem yapın (ör. eşzamansız) ve her şey yolunda gittiyse resolve'u, aksi takdirde reject'i çağırın.
Eski JavaScript'teki throw gibi, bir Error nesnesiyle reddetmek gelenekseldir ancak zorunlu değildir. Error nesnelerinin avantajı, yığın izlemeyi yakalayarak hata ayıklama araçlarını daha faydalı hale getirmeleridir.
Bu vaadi şu şekilde kullanabilirsiniz:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then(), biri başarılı durum, diğeri ise başarısız durum için 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ılı veya başarısız durum için geri çağırma ekleyebilirsiniz.
JavaScript Promise'leri, DOM'da "Future" olarak başladı, "Promise" olarak yeniden adlandırıldı ve son olarak JavaScript'e taşındı. Bunların DOM yerine JavaScript'te olması, Node.js gibi tarayıcı dışı JS bağlamlarında kullanılabilir olmaları açısından çok iyidir (temel API'lerinde kullanıp kullanmadıkları başka bir sorudur).
JavaScript özelliği olmalarına rağmen DOM bunları kullanmaktan çekinmez. Aslında, eş zamansız başarı/başarısızlık yöntemleri içeren tüm yeni DOM API'leri promise'leri kullanır. Bu durum Kota Yönetimi, Yazı Tipi Yükleme Etkinlikleri, ServiceWorker, Web MIDI, Akışlar ve daha fazlasında zaten yaşanıyor.
Diğer kitaplıklarla uyumluluk
JavaScript Promise API, then() yöntemine sahip her şeyi Promise benzeri (veya Promise dilinde thenable iç çekme) olarak değerlendirir. Bu nedenle, Q Promise döndüren bir kitaplık kullanırsanız yeni JavaScript Promise'leri ile sorunsuz çalışır.
Ancak daha önce de belirttiğim gibi jQuery'nin Deferred nesneleri pek kullanışlı değil. Neyse ki bunları standart Promise'lere dönüştürebilirsiniz. Bunu mümkün olan 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ğu için Promise.resolve(), bunu bir JavaScript Promise'ine dönüştürebilir. Ancak bazen ertelenenler, geri çağırmalarına 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 Promise'leri ise ilk değer dışındaki tüm değerleri yok sayar:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Neyse ki genellikle istediğiniz şey budur veya en azından istediğiniz şeye erişmenizi sağlar. Ayrıca jQuery'nin, reddetme işlemlerine Error nesneleri iletme kuralına uymadığını unutmayın.
Karmaşık eşzamansız kodlar artık daha kolay
Şimdi biraz kod yazalım. Diyelim ki şunları yapmak istiyoruz:
- Yükleme işlemini belirtmek için bir döner simge başlatın.
- Bir hikaye için JSON'u getirerek başlığı ve her bölümün URL'sini alın.
- Sayfaya başlık ekleme
- Her bölümü getir
- Hikayeyi sayfaya ekleme
- Çarkı durdurma
… ancak işlem sırasında bir sorun oluşursa kullanıcıya bunu da bildirin. Bu noktada dönen simgeyi de durdurmak isteriz. Yoksa dönmeye devam eder, başı döner ve başka bir kullanıcı arayüzüne çarpar.
Elbette bir hikaye sunmak için JavaScript kullanmazsınız, HTML olarak sunmak daha hızlıdır. Ancak bu kalıp, API'lerle çalışırken oldukça yaygındır: Birden fazla veri alma işlemi ve ardından her şey tamamlandığında bir işlem yapma.
Öncelikle ağdan veri getirme işlemini ele alalım:
XMLHttpRequest'i Promise'e dönüştürme
Eski API'ler, geriye dönük uyumlu bir şekilde mümkünse Promise'leri kullanacak şekilde güncellenecek. XMLHttpRequest, bu iş için ideal bir adaydır ancak şimdilik 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 bunu kullanalım:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Artık XMLHttpRequest ifadesini manuel olarak yazmadan HTTP istekleri yapabiliriz. Bu harika bir şey çünkü XMLHttpRequest ifadesinin sinir bozucu camel case kullanımını ne kadar az görürsem o kadar mutlu olurum.
Zincirleme
then(), hikayenin sonu değildir. Değerleri dönüştürmek veya ek eş zamansız işlemleri art arda çalıştırmak için then'leri birbirine bağlayabilirsiniz.
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 responseType kullanmak için get işlevimizi değiştirebiliriz ancak bunu Promise'ler ile de çö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ı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);
})
Aslında çok kolay bir şekilde getJSON() işlevi oluşturabiliriz:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON() yine bir söz döndürür. Bu söz, bir URL'yi getirir ve yanıtı JSON olarak ayrıştırır.
Eşzamansız işlemleri sıraya alma
Asenkron işlemleri sırayla çalıştırmak için then'leri zincirleyebilirsiniz.
then() geri çağırma işlevinden bir şey döndürdüğünüzde biraz sihirli bir durum ortaya çıkar.
Bir değer döndürürseniz sonraki then() bu değerle çağrılır. Ancak Promise benzeri bir şey döndürürseniz sonraki then() bunu bekler ve yalnızca bu Promise çözüldüğünde (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 istenecek bir dizi url verir ve ardından bunlardan ilkini isteriz. Bu noktada, Promise'ler basit geri çağırma kalıplarından gerçekten ayrılmaya başlar.
Bölümleri almak için bir kısayol yöntemi bile oluşturabilirsiniz:
var storyPromise;
function getChapter(i) {
storyPromise = storyPromise || getJSON('story.json');
return storyPromise.then(function(story) {
return getJSON(story.chapterUrls[i]);
})
}
// and using it is simple:
getChapter(0).then(function(chapter) {
console.log(chapter);
return getChapter(1);
}).then(function(chapter) {
console.log(chapter);
})
getChapter çağrılana kadar story.json indirilmez ancak getChapter sonraki çağrılışlarında hikaye vaadini yeniden kullandığımız için story.json yalnızca bir kez getirilir. Yaşasın Vaatler!
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 başarısızlık için (veya vaatler dilinde yerine getirme ve reddetme):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
catch() özelliğini de kullanabilirsiniz:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
catch() ile ilgili özel bir durum yoktur. then(undefined, func) için sadece bir kısaltmadır ancak daha kolay okunur. 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);
})
Fark küçük olsa da son derece faydalıdır. Promise reddi, bir ret geri çağırma işleviyle (veya eşdeğeri olduğu için catch() ile) sonraki then() öğesine atlar. then(func1, func2) ile func1 veya func2 çağrılır, ikisi birden çağrılmaz. Ancak then(func1).catch(func2) kullanıldığında, zincirdeki ayrı adımlar oldukları için func1 reddederse 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 try/catch'e çok benzer. "try" içinde gerçekleşen hatalar hemen catch() bloğuna gider. Yukarıdaki bilgileri akış şeması olarak da görebilirsiniz (akış şemalarını çok severim):
Yerine getirilen vaatler için mavi çizgileri, reddedilenler için kırmızı çizgileri takip edin.
JavaScript istisnaları ve promise'leri
Reddetme, bir Promise açıkça reddedildiğinde gerçekleşir. Ancak yapıcı geri çağırmada bir hata oluşursa örtük 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, Promise ile ilgili tüm işlemlerinizi Promise oluşturucu geri çağırma işlevinde yapmanız faydalı olur. Böylece hatalar otomatik olarak yakalanır ve reddedilir.
then() geri çağırmalarında oluşan hatalar için de aynı durum 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 bir hata göstermek için catch'i 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 veya kullanıcı çevrimdışıysa)
yanıtı JSON olarak ayrıştırmaya çalışan getJSON() içindeki de dahil olmak üzere sonraki tüm başarılı geri çağırmaları atlar ve ayrıca sayfaya chapter1.html ekleyen geri çağırmayı da atlar. 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 yapısında olduğu gibi, hata yakalanır ve sonraki kod çalışmaya devam eder. Böylece, dönen simge her zaman gizlenir. Bizim istediğimiz de budur. Yukarıdaki kod, aşağıdaki kodun engellenmeyen eş zamansız sürümü olur:
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ı amacıyla catch() kullanmak isteyebilirsiniz. Bunun için hatayı yeniden oluşturmanız 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: Her ikisinden de en iyi şekilde yararlanma
Asenkron düşünmek kolay değildir. Başlangıçta zorlanıyorsanız kodu eşzamanlıymış gibi yazmayı deneyin. Bu durumda:
try {
var story = getJSONSync('story.json');
addHtmlToPage(story.heading);
story.chapterUrls.forEach(function(chapterUrl) {
var chapter = getJSONSync(chapterUrl);
addHtmlToPage(chapter.html);
});
addTextToPage("All done");
}
catch (err) {
addTextToPage("Argh, broken: " + err.message);
}
document.querySelector('.spinner').style.display = 'none'
Bu yöntem işe yarıyor. Ancak senkronize çalışır ve indirme işlemi sırasında tarayıcıyı kilitler. Bunu eş zamansız hale getirmek için then() kullanarak işlemlerin sırayla gerçekleşmesini sağları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';
})
Peki bölüm URL'leri arasında nasıl döngü oluşturup bunları 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, asenkron işlemlerden haberdar değildir. Bu nedenle bölümlerimiz, indirilme sırasına göre gösterilir. Bu da aslında Pulp Fiction'ın yazılma şeklidir. Bu film Ucuz Roman değil, bu yüzden düzeltelim.
Sıra oluşturma
chapterUrls dizimizi bir Promise 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() ile ilk kez karşılaşıyoruz. Bu, verdiğiniz değere göre çözümlenen bir promise oluşturur. Promise örneği iletirseniz bunu döndürür (not: bu, bazı uygulamaların henüz uymadığı bir spesifikasyon değişikliğidir). Promise benzeri bir şey (then() yöntemi olan) iletirseniz aynı şekilde yerine getirilen/reddedilen gerçek bir Promise oluşturur. Başka bir değer (ör. Promise.resolve('Hello')) iletirseniz bu değerle yerine getirilen bir promise oluşturulur. Yukarıdaki gibi değer olmadan çağırırsanız "undefined" ile yerine getirilir.
Ayrıca, verdiğiniz değerle (veya tanımsız) reddeden bir Promise oluşturan Promise.reject(val) da 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, önceki örnekle aynı işlemi yapar ancak ayrı bir "sequence" değişkenine ihtiyaç duymaz. Reduce geri çağırma işlevimiz, dizideki her öğe için çağrılır.
"sequence" ilk seferde Promise.resolve() olur ancak diğer çağrılarda "sequence", önceki çağrıdan 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 Promise'dir.
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. Senkronize sürümün tamamen eş zamansız bir sürümünü oluşturduk. Ancak daha iyisini yapabiliriz. Şu anda sayfamız bu şekilde indiriliyor:
Tarayıcılar aynı anda birden fazla öğe indirme konusunda oldukça başarılıdır. Bu nedenle, bölümleri birbiri ardına indirerek performans kaybediyoruz. Bizim istediğimiz, hepsini aynı anda indirip hepsi geldikten sonra işlemektir. Neyse ki bunun için bir API var:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all, bir dizi promise alır ve bunların tümü başarıyla tamamlandığında yerine getirilen bir promise oluşturur. Sonuçları (yerine getirilen vaatler ne olursa olsun), ilettiğiniz vaatlerle aynı sırada bir dizi olarak 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üklemeye kıyasla birkaç saniye daha hızlı olabilir ve ilk denememize göre daha az kod içerir. Bölümler herhangi bir sırayla indirilebilir ancak ekranda doğru sırayla görünür.
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 yayınlandığında, kullanıcı ikinci bölümün eksik olduğunu fark etmeyebileceğinden 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, 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';
})
İşte bu kadar, her iki dünyanın en iyisi! Tüm içeriğin yayınlanması aynı süreyi alır ancak kullanıcı içeriğin ilk bölümü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ümlerle daha belirgin hale gelir.
Yukarıdaki işlemi Node.js tarzı geri çağırmalar veya etkinliklerle yapmak, kod miktarını yaklaşık iki katına çıkarır. Daha da önemlisi, bu yöntemi takip etmek o kadar kolay değildir. Ancak promise'ler için hikaye burada bitmiyor. Diğer ES6 özellikleriyle birleştirildiğinde daha da kolaylaşıyorlar.
Bonus turu: Kapsamlı özellikler
Bu makaleyi ilk yazdığımdan beri Promise'leri kullanma imkanı 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 olanak tanıyor ancak ana iş parçacığını engellemiyor. Bu konu hakkında daha fazla bilgiyi asenkron işlevler makalemde bulabilirsiniz. Büyük tarayıcılarda hem Promise'ler hem de eş zamansız işlevler için yaygın destek vardır. Ayrıntıları MDN'nin Promise ve async işlevi referansında bulabilirsiniz.
Bu makaleyi okuyup düzeltmeler ve önerilerde bulunan 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 teşekkür ederiz.