Sözler, ertelenen ve eşzamansız hesaplamaları basitleştirir. Promise, henüz tamamlanmamış bir işlemi temsil eder.
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
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:
- Yüklemeyi belirtmek için bir döner simge başlatma
- Bir hikaye için JSON'u getiriyoruz. Bu JSON, bize hikayenin başlığını ve her bölümün URL'sini verir.
- Sayfaya başlık ekleme
- Her bölümü getirme
- Hikayeyi sayfaya ekleme
- 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.