Promise menyederhanakan komputasi yang tertunda dan asinkron. Promise mewakili operasi yang belum selesai.
Para developer, bersiaplah untuk momen penting dalam sejarah pengembangan web.
[Drumroll begins]
Promise telah hadir di JavaScript!
[Kembang api meletus, kertas warna-warni bertaburan dari atas, kerumunan orang bersuka ria]
Pada saat ini Anda termasuk dalam salah satu kategori ini:
- Orang-orang bersuka cita di sekeliling Anda, tetapi Anda tidak tahu apa yang sedang diramaikan. Mungkin Anda bahkan tidak yakin apa yang dimaksud dengan "janji". Anda akan mengangkat bahu, tetapi berat kertas warna-warni membebani bahu Anda. Jika begitu, jangan khawatir, saya perlu waktu lama untuk mengetahui mengapa harus memedulikannya. Anda mungkin ingin memulai dari awal.
- Anda mengangkat tinju! Sudah saatnya kan? Anda pernah menggunakan Promise ini sebelumnya, tetapi Anda heran karena semua implementasi memiliki API yang sedikit berbeda. API apa untuk versi resmi JavaScript? Sebaiknya Anda mulai dengan terminologi.
- Anda sudah tahu tentang hal ini dan Anda mengejek mereka yang melompat kegirangan seperti berita baru bagi mereka. Luangkan waktu sejenak untuk mematangkan superioritas Anda, lalu langsung buka referensi API.
Dukungan browser dan polyfill
Untuk meningkatkan browser yang tidak memiliki implementasi lengkap promise agar memenuhi spesifikasi, atau menambahkan promise ke browser lain dan Node.js, lihat polyfill (file gzip 2k).
Ada apa sebenarnya?
JavaScript merupakan thread tunggal, yang berarti bahwa dua bit skrip tidak dapat dijalankan secara bersamaan; keduanya harus dijalankan satu per satu. Di browser, JavaScript berbagi thread dengan banyak hal lain yang berbeda dari satu browser ke browser. Namun, biasanya JavaScript berada dalam antrean yang sama dengan menggambar, memperbarui gaya, dan menangani tindakan pengguna (seperti menandai teks dan berinteraksi dengan kontrol formulir). Aktivitas dalam salah satu tindakan ini akan menunda aktivitas lainnya.
Sebagai manusia, Anda memiliki banyak thread. Anda dapat mengetik dengan beberapa jari, mengemudi, dan melakukan percakapan sekaligus. Satu-satunya fungsi pemblokiran yang harus kita tangani adalah bersin, karena semua aktivitas saat ini harus ditangguhkan selama bersin. Hal ini tentu sangat mengganggu, terutama saat Anda sedang mengemudi dan akan melakukan percakapan. Anda tentu tidak ingin menulis kode yang sering bersin.
Anda mungkin sudah menggunakan peristiwa dan callback untuk menyiasatinya. Berikut adalah peristiwa tersebut:
var img1 = document.querySelector('.img-1');
img1.addEventListener('load', function() {
// woo yey image loaded
});
img1.addEventListener('error', function() {
// argh everything's broken
});
Ini sama sekali tidak sering bersin. Kita mendapatkan gambar, menambahkan beberapa pemroses, lalu JavaScript dapat menghentikan eksekusi hingga salah satu pemroses tersebut dipanggil.
Sayangnya, dalam contoh di atas, mungkin saja peristiwa tersebut terjadi sebelum kita mulai memprosesnya, jadi kita perlu mengatasinya menggunakan properti "complete" gambar tersebut:
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
});
Tindakan ini tidak menangkap gambar yang mengalami error sebelum kita sempat memprosesnya; sayangnya DOM tidak memberi kita cara untuk melakukannya. Selain itu, ini memuat satu gambar. Hal ini akan semakin kompleks jika kita ingin mengetahui kapan serangkaian gambar dimuat.
Peristiwa tidak selalu menjadi cara terbaik
Peristiwa sangat cocok untuk hal-hal yang dapat terjadi beberapa kali pada objek yang sama—keyup
, touchstart
, dll. Dengan peristiwa tersebut, Anda tidak terlalu peduli dengan apa yang terjadi sebelum melampirkan pemroses. Namun, jika menyangkut
keberhasilan/kegagalan asinkron, idealnya Anda memerlukan sesuatu seperti ini:
img1.callThisIfLoadedOrWhenLoaded(function() {
// loaded
}).orIfFailedCallThis(function() {
// failed
});
// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
// all loaded
}).orIfSomeFailedCallThis(function() {
// one or more failed
});
Inilah yang dilakukan promise, namun dengan penamaan yang lebih baik. Jika elemen gambar HTML memiliki metode "ready" yang menampilkan promise, kita dapat melakukan hal ini:
img1.ready()
.then(function() {
// loaded
}, function() {
// failed
});
// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
// all loaded
}, function() {
// one or more failed
});
Pada dasarnya, promise mirip dengan pemroses peristiwa, hanya saja:
- Promise hanya bisa berhasil atau gagal satu kali. Promise tidak dapat gagal atau berhasil dua kali, juga tidak dapat beralih dari berhasil ke gagal atau sebaliknya.
- Jika promise berhasil atau gagal dan Anda kemudian menambahkan callback berhasil/gagal, callback yang benar akan dipanggil, meskipun peristiwa tersebut terjadi lebih awal.
Hal ini sangat berguna untuk keberhasilan/kegagalan asinkron, karena Anda menjadi kurang tertarik dengan waktu persis sesuatu menjadi tersedia, dan lebih tertarik dengan reaksi terhadap hasilnya.
Terminologi promise
Domenic Denicola telah memeriksa draf pertama artikel ini dan memberi saya nilai "F" untuk terminologi. Ia menahan saya, memaksa saya menyalin States and Fates 100 kali, dan menulis surat yang mencemaskan kepada orang tua saya. Meskipun begitu, saya masih mencampuradukkan banyak terminologi, tetapi berikut ini dasar-dasarnya:
Promise dapat berupa:
- fulfilled - Tindakan yang terkait dengan promise berhasil
- rejected - Tindakan yang terkait dengan promise gagal
- pending - Belum terpenuhi atau ditolak
- selesai - Telah terpenuhi atau ditolak
Spesifikasi
juga menggunakan istilah thenable untuk mendeskripsikan objek yang mirip promise,
karena memiliki metode then
. Istilah ini mengingatkan saya pada mantan Manajer Sepak Bola
Inggris Terry Venables jadi
saya akan menggunakannya sesedikit mungkin.
Promise hadir di JavaScript!
Promise sudah lama ada dalam bentuk library, misalnya:
Promise di atas dan promise JavaScript menggunakan perilaku umum dan terstandardisasi yang disebut Promises/A+. Jika Anda pengguna jQuery, mereka memiliki sesuatu yang mirip yang disebut Deferreds. Namun, jQuery juga memiliki jenis Promise, tetapi ini hanyalah subset Deferred dan memiliki masalah yang sama.
Meskipun implementasi promise mengikuti perilaku terstandardisasi, API keseluruhannya berbeda. Promise JavaScript mirip di API dengan RSVP.js. Berikut cara membuat promise:
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"));
}
});
Konstruktor promise menggunakan satu argumen, callback dengan dua parameter, resolve dan reject. Lakukan sesuatu dalam callback, bisa asinkron, lalu panggil resolve jika semuanya berjalan lancar, jika tidak, panggil reject.
Seperti throw
di JavaScript lama, ini adalah kebiasaan, tetapi tidak diperlukan, untuk menolak dengan objek Error. Manfaat objek Error adalah merekam pelacakan tumpukan, sehingga membuat alat debug jadi lebih berguna.
Berikut cara menggunakan promise tersebut:
promise.then(function(result) {
console.log(result); // "Stuff worked!"
}, function(err) {
console.log(err); // Error: "It broke"
});
then()
menggunakan dua argumen, satu callback untuk kasus berhasil, dan satu lagi untuk kasus gagal. Keduanya bersifat opsional, jadi Anda dapat menambahkan callback untuk
kasus berhasil atau gagal saja.
Promise JavaScript dimulai di DOM sebagai "Futures", diganti namanya menjadi "Promises", dan terakhir dimasukkan ke JavaScript. Keberadaannya di JavaScript, bukan DOM, bagus sekali karena promise tersebut akan tersedia dalam konteks JS non-browser seperti Node.js (lain lagi masalahnya jika menggunakannya dalam API intinya).
Meskipun menjadi fitur JavaScript, DOM tidak takut menggunakannya. Sebenarnya, semua DOM API baru dengan metode berhasil/gagal asinkron akan menggunakan promise. Hal ini sudah terjadi pada Quota Management, Font Load Events, ServiceWorker, Web MIDI, Streams, dan lainnya.
Kompatibilitas dengan library lain
API promise JavaScript akan memperlakukan apa pun dengan metode then()
sebagai
mirip promise (atau thenable
dalam istilah promise sigh), jadi jika Anda menggunakan library
yang menampilkan promise Q, tidak masalah, library tersebut akan berfungsi dengan baik dengan promise
JavaScript baru.
Walaupun, seperti yang saya sebutkan, Deferred di jQuery agaknya… kurang berguna. Untungnya, Anda dapat mentransmisikannya ke promise standar, yang patut dilakukan sesegera mungkin:
var jsPromise = Promise.resolve($.ajax('/whatever.json'))
Di sini, $.ajax
jQuery menampilkan Deferred. Karena memiliki metode then()
, Promise.resolve()
dapat mengubahnya menjadi promise JavaScript. Namun,
terkadang deferred meneruskan beberapa argumen ke callback-nya, misalnya:
var jqDeferred = $.ajax('/whatever.json');
jqDeferred.then(function(response, statusText, xhrObj) {
// ...
}, function(xhrObj, textStatus, err) {
// ...
})
Promise JS mengabaikan semua hal kecuali yang pertama:
jsPromise.then(function(response) {
// ...
}, function(xhrObj) {
// ...
})
Untungnya inilah yang biasanya Anda inginkan, atau setidaknya memberi Anda akses ke hal yang Anda inginkan. Selain itu, perlu diperhatikan bahwa jQuery tidak mengikuti konvensi penerusan objek Error ke dalam penolakan.
Kode asinkron yang rumit menjadi lebih mudah
Baiklah, mari kita buat kode. Misalnya, kita ingin:
- Memulai indikator lingkaran berputar untuk menunjukkan pemuatan
- Ambil sebagian JSON untuk cerita, yang akan memberi kita judul dan URL untuk tiap bab
- Menambahkan judul ke halaman
- Mengambil setiap bab
- Menambahkan cerita ke halaman
- Hentikan indikator lingkaran berputar
… tetapi beri tahu juga pengguna jika terjadi masalah di tengah proses. Kita juga perlu menghentikan indikator lingkaran berputar pada saat itu, jika tidak, indikator tersebut akan terus berputar, menjadi pusing, dan menabrak UI lainnya.
Tentu saja, Anda tidak akan menggunakan JavaScript untuk menyajikan cerita, lebih cepat jika berfungsi sebagai HTML, tetapi pola ini sangat umum jika berkaitan dengan API: Beberapa pengambilan data, lalu lakukan sesuatu setelah semuanya selesai.
Untuk memulai, mari kita tangani pengambilan data dari jaringan:
Mem-promise-kan XMLHttpRequest
API lama akan diupdate untuk menggunakan promise, jika memungkinkan dengan cara yang kompatibel dengan versi lama. XMLHttpRequest
adalah kandidat utama, tetapi untuk sementara,
mari kita tulis fungsi sederhana untuk membuat permintaan GET:
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();
});
}
Sekarang mari kita gunakan:
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.error("Failed!", error);
})
Sekarang kita dapat membuat permintaan HTTP tanpa mengetik XMLHttpRequest
secara manual. Ini bagus karena
semakin sedikit saya harus melihat cara penulisan camel-casing XMLHttpRequest
yang menjengkelkan, semakin bahagia hidup saya.
Perantaian
then()
bukanlah akhir cerita, Anda dapat merantai then
bersama untuk mentransformasikan nilai atau menjalankan tindakan asinkron tambahan satu per satu.
Mentransformasi nilai
Anda dapat mengubah nilai hanya dengan menampilkan nilai baru:
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
})
Sebagai contoh praktis, mari kita kembali ke:
get('story.json').then(function(response) {
console.log("Success!", response);
})
Responsnya berupa JSON, tetapi saat ini kami menerimanya sebagai teks biasa. Kita
dapat mengubah fungsi get untuk menggunakan JSON
responseType
,
tetapi kita juga dapat memecahkannya dalam konteks promise:
get('story.json').then(function(response) {
return JSON.parse(response);
}).then(function(response) {
console.log("Yey JSON!", response);
})
Karena JSON.parse()
mengambil satu argumen dan menampilkan nilai hasil transformasi,
kita dapat membuat pintasan:
get('story.json').then(JSON.parse).then(function(response) {
console.log("Yey JSON!", response);
})
Bahkan, kita dapat membuat fungsi getJSON()
dengan sangat mudah:
function getJSON(url) {
return get(url).then(JSON.parse);
}
getJSON()
tetap menampilkan promise, promise yang mengambil URL, lalu mengurai
respons sebagai JSON.
Mengantre tindakan asinkron
Anda juga dapat merantai then
untuk menjalankan tindakan asinkron secara berurutan.
Jika Anda menampilkan sesuatu dari callback then()
, ini agak ajaib.
Jika Anda menampilkan nilai, then()
berikutnya akan dipanggil dengan nilai tersebut. Namun,
jika Anda menampilkan sesuatu yang mirip promise, then()
berikutnya akan menunggunya, dan
hanya dipanggil saat promise tersebut selesai (berhasil/gagal). Contoh:
getJSON('story.json').then(function(story) {
return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
console.log("Got chapter 1!", chapter1);
})
Di sini kita membuat permintaan asinkron ke story.json
, yang memberi kita satu set URL untuk diminta, lalu kita meminta URL pertama. Inilah saatnya promise
benar-benar mulai unggul dari pola callback biasa.
Anda bahkan dapat membuat metode pintasan untuk mendapatkan bab:
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);
})
Kita tidak mendownload story.json
hingga getChapter
dipanggil, tetapi saat
getChapter
dipanggil lagi, kita akan menggunakan kembali promise cerita, sehingga story.json
hanya diambil sekali. Asyik, Janji!
Penanganan error
Seperti yang telah kita lihat sebelumnya, then()
menggunakan dua argumen, satu untuk berhasil, satu
untuk gagal (atau fulfill dan reject, dalam istilah promise):
get('story.json').then(function(response) {
console.log("Success!", response);
}, function(error) {
console.log("Failed!", error);
})
Anda juga dapat menggunakan catch()
:
get('story.json').then(function(response) {
console.log("Success!", response);
}).catch(function(error) {
console.log("Failed!", error);
})
Tidak ada yang spesial dengan catch()
, ini hanya pemanis untuk
then(undefined, func)
, tetapi lebih mudah dibaca. Perhatikan bahwa dua contoh kode di atas tidak berperilaku sama, kode yang terakhir setara dengan:
get('story.json').then(function(response) {
console.log("Success!", response);
}).then(undefined, function(error) {
console.log("Failed!", error);
})
Perbedaannya kecil, tetapi sangat berguna. Penolakan promise akan melompat
maju ke then()
berikutnya dengan callback penolakan (atau catch()
, karena
setara). Dengan then(func1, func2)
, func1
atau func2
akan
dipanggil, tidak pernah keduanya. Namun, dengan then(func1).catch(func2)
, keduanya akan
dipanggil jika func1
menolak, karena keduanya adalah langkah terpisah dalam rantai. Perhatikan
hal berikut:
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!");
})
Alur di atas sangat mirip dengan try/catch JavaScript normal, error yang terjadi dalam "try" langsung masuk ke blok catch()
. Berikut ini
diagram alur untuk hal di atas (karena saya suka diagram alur):
Ikuti garis biru untuk promise yang terpenuhi, atau merah untuk promise yang menolak.
Pengecualian dan promise JavaScript
Penolakan terjadi jika promise secara eksplisit ditolak, tetapi juga secara implisit jika error ditampilkan dalam callback konstruktor:
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);
})
Artinya, sebaiknya lakukan semua pekerjaan terkait promise di dalam callback konstruktor promise, sehingga error otomatis ditangkap dan menjadi penolakan.
Hal yang sama berlaku untuk error yang ditampilkan dalam callback then()
.
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);
})
Penanganan error dalam praktik
Dengan cerita dan bab, kita dapat menggunakan catch untuk menampilkan error kepada pengguna:
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';
})
Jika pengambilan story.chapterUrls[0]
gagal (misalnya, http 500 atau pengguna sedang offline), semua callback berhasil berikut akan dilewati, termasuk yang ada di getJSON()
yang mencoba mengurai respons sebagai JSON, dan juga akan melewati callback yang menambahkan bab1.html ke halaman. Sebagai gantinya, beralih ke callback
catch. Akibatnya, "Failed to show bab" akan ditambahkan ke halaman jika
tindakan sebelumnya ada yang gagal.
Seperti try/catch di JavaScript, error akan ditangkap dan kode selanjutnya akan dilanjutkan, sehingga indikator lingkaran berputar selalu tersembunyi, seperti yang kita inginkan. Kode di atas menjadi versi asinkron yang tidak memblokir untuk:
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'
Sebaiknya Anda melakukan catch()
hanya untuk tujuan logging, tanpa melakukan pemulihan dari error. Untuk melakukannya, cukup tampilkan kembali error. Kita dapat melakukannya dalam
metode getJSON()
:
function getJSON(url) {
return get(url).then(JSON.parse).catch(function(err) {
console.log("getJSON failed for", url, err);
throw err;
});
}
Jadi kita telah berhasil mengambil satu bab, tetapi kita menginginkan semuanya. Mari kita lakukan.
Paralelisme dan pengurutan: mendapatkan yang terbaik dari keduanya
Membayangkan asinkron tidaklah mudah. Jika Anda berusaha keras untuk memulai, coba tulis kode seakan-akan kode tersebut sinkron. Dalam hal ini:
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'
Berhasil. Namun kode ini menyinkronkan dan mengunci browser saat ada yang didownload. Untuk
membuat hal ini berfungsi secara asinkron, kita menggunakan then()
untuk melakukannya satu per satu.
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';
})
Namun, bagaimana kita dapat melakukan loop melalui URL bab dan mengambilnya secara berurutan? Hal ini tidak akan berhasil:
story.chapterUrls.forEach(function(chapterUrl) {
// Fetch chapter
getJSON(chapterUrl).then(function(chapter) {
// and add it to the page
addHtmlToPage(chapter.html);
});
})
forEach
tidak mengenal asinkron, jadi bab kita akan muncul sesuai urutan
downloadnya, yang pada dasarnya seperti cara menulis Pulp Fiction. Ini bukan
Pulp Fiction, jadi mari kita perbaiki.
Membuat urutan
Kita ingin mengubah array chapterUrls
menjadi urutan promise. Kita dapat melakukannya menggunakan then()
:
// 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);
});
})
Ini adalah pertama kalinya kita melihat Promise.resolve()
, yang membuat promise yang me-resolve ke nilai apa pun yang Anda berikan. Jika Anda meneruskan
instance Promise
, instance tersebut akan ditampilkan (catatan: ini adalah
perubahan pada spesifikasi yang belum diikuti oleh beberapa implementasi). Jika Anda
meneruskan sesuatu yang seperti promise (memiliki metode then()
), akan dibuat
Promise
asli yang memenuhi/menolak dengan cara yang sama. Jika Anda meneruskan
nilai lain, misalnya, Promise.resolve('Hello')
, fungsi ini membuat
promise yang terpenuhi dengan nilai tersebut. Jika Anda memanggilnya tanpa nilai,
seperti di atas, kueri tersebut akan terpenuhi dengan "undefined".
Ada juga Promise.reject(val)
, yang membuat promise yang ditolak dengan
nilai yang Anda berikan (atau undefined).
Kita dapat merapikan kode di atas menggunakan
array.reduce
:
// 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())
Ini melakukan hal yang sama dengan contoh sebelumnya, tetapi tidak memerlukan variabel
"sequence" terpisah. Callback reduce ditampilkan untuk tiap item dalam array.
"sequence" adalah Promise.resolve()
pertama kali, tetapi untuk panggilan berikutnya, "sequence" adalah apa pun yang kita kembalikan dari panggilan sebelumnya. array.reduce
sangat berguna untuk meringkas array menjadi satu nilai, yang dalam hal ini
adalah promise.
Mari kita rangkum semuanya:
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';
})
Dan sekarang kita memilikinya, versi asinkron penuh dari versi sinkron. Namun, kita dapat melakukannya dengan lebih baik. Untuk saat ini, halaman kita akan didownload seperti ini:
Browser lumayan bagus dalam mendownload beberapa item sekaligus, jadi kita akan kehilangan performa dengan mendownload bab satu per satu. Yang ingin kita lakukan adalah mengunduh semuanya sekaligus, lalu memprosesnya setelah semuanya tiba. Untungnya ada API untuk ini:
Promise.all(arrayOfPromises).then(function(arrayOfResults) {
//...
})
Promise.all
menggunakan array promise dan membuat promise yang akan terpenuhi ketika semuanya berhasil diselesaikan. Anda mendapatkan array hasil (apa pun
promise yang dipenuhi) dalam urutan yang sama dengan promise yang Anda teruskan.
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';
})
Bergantung pada koneksi, pemuatannya bisa beberapa detik lebih cepat daripada memuatnya satu per satu, dan kodenya lebih sedikit dari percobaan pertama kita. Bab dapat didownload dalam urutan apa pun, tetapi muncul di layar dalam urutan yang tepat.
Namun, kita tetap dapat meningkatkan performa yang dirasakan. Ketika bab satu masuk, kita harus menambahkannya ke laman. Hal ini memungkinkan pengguna mulai membaca sebelum bab selanjutnya tiba. Jika bab tiga masuk, kita tidak akan menambahkannya ke halaman karena pengguna mungkin tidak menyadari bab dua tidak ada. Jika bab dua masuk, kita bisa menambahkan bab dua dan tiga, dst.
Caranya, kita mengambil JSON untuk semua bab sekaligus, lalu membuat urutan untuk menambahkannya ke dokumen:
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';
})
Dan ini dia, yang terbaik dari keduanya! Dibutuhkan waktu yang sama untuk mengirim semua konten, tetapi pengguna mendapatkan bagian dari konten pertama lebih cepat.
Dalam contoh sepele ini, semua bab tiba kurang lebih sama waktunya, tetapi manfaat menampilkannya satu per satu akan menjadi berlebihan pada bab yang lebih banyak dan lebih besar.
Melakukan hal di atas dengan callback atau peristiwa bergaya Node.js akan menggandakan kode, tetapi yang lebih penting adalah tidak mudah diikuti. Namun, ini bukan akhir cerita untuk promise, jika dikombinasikan dengan fitur ES6 lainnya, maka akan lebih mudah lagi.
Babak bonus: kemampuan yang diperluas
Sejak saya pertama kali menulis artikel ini, kemampuan untuk menggunakan Promise telah berkembang secara signifikan. Sejak Chrome 55, fungsi asinkron telah memungkinkan kode berbasis promise ditulis seakan-akan sinkron, tetapi tanpa memblokir thread utama. Anda dapat membaca hal ini lebih lanjut di artikel fungsi asinkron saya. Ada dukungan yang luas untuk Promise dan fungsi asinkron di browser utama. Anda dapat menemukan detailnya di referensi Promise dan fungsi asinkron MDN.
Terima kasih banyak untuk Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans, dan Yutaka Hirano yang telah memeriksa dan membuat koreksi/saran.
Terima kasih juga kepada Mathias Bynens yang telah memperbarui berbagai bagian dari artikel ini.