Promise JavaScript: pengantar

Promise menyederhanakan komputasi yang ditangguhkan dan asinkron. Promise mewakili operasi yang belum selesai.

Jake Archibald
Jake Archibald

Para developer, bersiaplah untuk momen penting dalam sejarah pengembangan web.

[Drumroll dimulai]

Promise telah hadir di JavaScript!

[Kembang api meledak, kertas berkilau berjatuhan dari atas, kerumunan menjadi ramai]

Pada titik ini, Anda termasuk dalam salah satu kategori berikut:

  • Orang-orang bersorak-sorai di sekitar Anda, tetapi Anda tidak yakin tentang apa yang sedang ramai. Mungkin Anda bahkan tidak yakin apa itu "janji". Anda akan mengangkat bahu, tetapi berat kertas berkilauan membebani bahu Anda. Jika demikian, jangan khawatirkan. Saya butuh waktu lama untuk mencari tahu mengapa saya harus mempedulikan hal ini. Sebaiknya Anda memulai dari awal.
  • Anda mengangkat tinju! Sudah waktunya, bukan? Anda pernah menggunakan Promise ini sebelumnya, tetapi Anda merasa terganggu karena semua implementasi memiliki API yang sedikit berbeda. Apa API untuk versi JavaScript resmi? Sebaiknya Anda mulai dengan terminologi.
  • Anda sudah tahu tentang hal ini dan Anda mengejek mereka yang melompat-lompat kegirangan mereka menyukai berita ini. Luangkan waktu sejenak untuk mematangkan superioritas Anda, lalu langsung buka referensi API.

Dukungan browser dan polyfill

Dukungan Browser

  • 32
  • 12
  • 29
  • 8

Sumber

Untuk meningkatkan browser yang tidak memiliki implementasi lengkap promise ke kepatuhan spesifikasi, atau menambahkan promise ke browser lain dan Node.js, lihat polyfill (file gzip 2k).

Ada apa sebenarnya?

JavaScript memiliki thread tunggal, yang berarti bahwa dua bit skrip tidak dapat dijalankan 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 melukis, 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 di saat yang sama. Satu-satunya fungsi pemblokiran yang harus kita tangani adalah bersin, karena semua aktivitas saat ini harus ditangguhkan selama bersin. Hal ini cukup menjengkelkan, terutama saat Anda mengemudi dan mencoba melakukan percakapan. Anda tentu tidak ingin menulis kode yang sering bersin.

Anda mungkin telah menggunakan peristiwa dan callback untuk mengatasinya. Berikut adalah peristiwanya:

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

Ini tidak sering bersin. Kita mendapatkan gambar, menambahkan beberapa pemroses, lalu JavaScript dapat berhenti mengeksekusi hingga salah satu pemroses tersebut dipanggil.

Sayangnya, dalam contoh di atas, mungkin saja peristiwa terjadi sebelum kita mulai memprosesnya, jadi kita perlu mengatasinya dengan 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
});

Metode ini tidak menangkap gambar yang mengalami error sebelum kita sempat memprosesnya; sayangnya DOM tidak memberi kita cara untuk melakukannya. Selain itu, kode ini memuat satu gambar. Hal ini akan menjadi lebih kompleks jika kita ingin mengetahui kapan serangkaian gambar dimuat.

Peristiwa tidak selalu menjadi cara terbaik

Peristiwa sangat cocok untuk hal-hal yang bisa terjadi beberapa kali pada objek yang sama—keyup, touchstart, dll. Dengan peristiwa tersebut, Anda tidak terlalu peduli dengan apa yang terjadi sebelum memasang 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 "siap" yang menampilkan promise, kita dapat melakukan 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 seperti pemroses peristiwa, kecuali:

  • Promise hanya bisa berhasil atau gagal satu kali. Kegagalan atau kegagalan tidak bisa 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.

Cara ini sangat berguna untuk keberhasilan/kegagalan asinkron, karena Anda menjadi kurang tertarik dengan waktu persis sesuatu menjadi tersedia, dan lebih tertarik untuk bereaksi 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 Status dan Nasib 100 kali, dan menulis surat yang mengkhawatirkan kepada orang tua saya. Meskipun begitu, saya masih bingung dengan 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 Britania, Terry Venables, jadi saya akan menggunakannya sesedikit mungkin.

Promise hadir di JavaScript!

Promise telah lama ada dalam bentuk library, seperti:

Promise di atas dan promise JavaScript memiliki perilaku standar dan umum yang disebut Promises/A+. Jika Anda pengguna jQuery, keduanya memiliki perilaku yang mirip, yang disebut Deferred. Namun, Deferred tidak sesuai dengan Promise/A+, yang membuatnya sedikit berbeda dan kurang berguna, jadi berhati-hatilah. jQuery juga memiliki jenis Promise, tetapi ini hanyalah subset Ditangguhkan dan memiliki masalah yang sama.

Meskipun implementasi promise mengikuti perilaku standar, 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 mengambil satu argumen, callback dengan dua parameter, resolver dan tolak. Lakukan sesuatu dalam callback, mungkin asinkron, lalu panggil resolve jika semuanya berfungsi, jika tidak, panggil tolak.

Seperti throw di JavaScript lama biasa, penolakan dengan objek Error merupakan hal yang biasa, tetapi tidak diperlukan. Manfaat objek Error adalah merekam pelacakan tumpukan, sehingga alat proses debug menjadi lebih bermanfaat.

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, callback untuk kasus berhasil, dan satu lagi untuk kasus gagal. Keduanya bersifat opsional, jadi Anda dapat menambahkan callback hanya untuk kasus berhasil atau gagal.

Promise JavaScript dimulai di DOM sebagai "Futures", diganti namanya menjadi "Promises", dan terakhir dipindahkan ke JavaScript. Keberadaannya di JavaScript, bukan DOM, sangat tepat karena akan tersedia dalam konteks JS non-browser seperti Node.js (masalah lain lain adalah apakah kode tersebut digunakan di API intinya atau tidak).

Meski fitur ini JavaScript, DOM tidak takut untuk menggunakannya. Bahkan, 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

Promise API JavaScript akan memperlakukan apa pun dengan metode then() sebagai seperti promise (atau thenable dalam suara berjanji), jadi jika Anda menggunakan library yang menampilkan promise Q, tidak masalah, ini akan cocok dengan promise JavaScript baru.

Meskipun, seperti yang saya sebutkan, Deferred jQuery sedikit... tidak membantu. 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 dependensi 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. Perlu diketahui juga bahwa jQuery tidak mengikuti konvensi penerusan objek Error ke penolakan.

Kode asinkron yang rumit menjadi lebih mudah

Baiklah, mari kita buat kode beberapa hal. Misalnya kita ingin:

  1. Memulai indikator lingkaran berputar untuk menunjukkan pemuatan
  2. Ambil beberapa JSON untuk cerita, yang memberi kita judul dan URL untuk setiap bab
  3. Tambahkan judul ke halaman
  4. Ambil setiap bab
  5. Tambahkan cerita ke halaman
  6. Hentikan indikator lingkaran berputar

... tetapi juga beri tahu pengguna jika terjadi masalah dalam prosesnya. Kita juga perlu menghentikan indikator lingkaran berputar pada saat itu, jika tidak, tombol akan terus berputar, menjadi pusing, dan menabrak UI lainnya.

Tentu saja, Anda tidak akan menggunakan JavaScript untuk menyampaikan cerita, lebih cepat ditayangkan sebagai HTML, tetapi pola ini cukup umum saat berurusan dengan API: Beberapa pengambilan data, lalu lakukan sesuatu setelah selesai.

Untuk memulai, mari kita tangani pengambilan data dari jaringan:

Mem-janjikan 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 camel-casing XMLHttpRequest yang menyebalkan, semakin bahagia hidup saya.

Perantaian

then() bukanlah akhir dari cerita, Anda dapat menggabungkan then untuk mengubah 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 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 menyelesaikannya di halaman 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 yang telah diubah, 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() masih menampilkan promise, promise yang mengambil URL lalu mengurai respons sebagai JSON.

Mengantrekan tindakan asinkron

Anda juga dapat membuat rantai then untuk menjalankan tindakan asinkron secara berurutan.

Saat Anda menampilkan sesuatu dari callback then(), ini sedikit ajaib. Jika Anda menampilkan sebuah 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 sekumpulan URL untuk diminta, lalu kami meminta URL pertama. Pada tahap ini, promise benar-benar mulai menarik perhatian 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 sampai getChapter dipanggil, tetapi jika getChapter dipanggil lagi, kita akan menggunakan kembali promise cerita, sehingga story.json hanya diambil satu kali. Asyik, Janji!

Penanganan error

Seperti yang telah kita lihat sebelumnya, then() menggunakan dua argumen, satu untuk berhasil, satu untuk gagal (atau terpenuhi dan tolak, dalam kata 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 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 diarahkan ke blok catch(). Seperti ini contohnya di atas sebagai diagram alir (karena saya suka diagram alir):

Ikuti garis biru untuk promise yang terpenuhi, atau merah untuk promise yang ditolak.

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 callback di getJSON() yang mencoba mengurai respons sebagai JSON, dan juga 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 coba/tangkapan JavaScript, error akan ditangkap dan kode berikutnya 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 berhasil mengambil satu bab, tetapi kita menginginkan semuanya. Mari kita lakukan.

Paralelisme dan pengurutan: mendapatkan yang terbaik dari keduanya

Memikirkan asinkron tidak mudah. Jika Anda kesulitan untuk memulai, coba tulis kode seolah-olah kode tersebut adalah sinkron. Dalam kasus 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. Tapi itu disinkronkan dan mengunci browser saat segala sesuatunya diunduh. Untuk membuat tugas ini 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 cara kita memutar URL segmen dan mengambilnya secara berurutan? Hal ini tidak berfungsi:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach tidak sadar asinkron, jadi bab kita akan muncul sesuai urutan downloadnya, yang pada dasarnya seperti cara penulisan Pulp Fiksi. Ini bukanlah Pulp Fiksi, 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 untuk di-resolve ke nilai apa pun yang Anda berikan. Jika Anda meneruskan instance Promise, Anda hanya akan menampilkannya (catatan: ini adalah perubahan pada spesifikasi yang belum diikuti oleh beberapa implementasi). Jika Anda meneruskannya sesuatu yang mirip promise (memiliki metode then()), metode ini akan membuat 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 akan terpenuhi dengan "undefined".

Ada juga Promise.reject(val), yang membuat promise yang akan ditolak dengan nilai yang Anda berikan padanya (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())

Tindakan ini sama seperti contoh sebelumnya, tetapi tidak memerlukan variabel "sequence" terpisah. Callback reduksi dipanggil untuk setiap item dalam array. "urutan" adalah Promise.resolve() kali pertama, tetapi untuk panggilan lainnya, "urutan" adalah apa pun yang kita tampilkan dari panggilan sebelumnya. array.reduce sangat berguna untuk meringkas array menjadi satu nilai, yang dalam hal ini adalah promise.

Mari kita satukan 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 akhirnya, versi yang sepenuhnya asinkron. Tapi, kita bisa melakukan dengan lebih baik lagi. Untuk saat ini, halaman kita didownload seperti ini:

Browser cukup bagus dalam mendownload beberapa item sekaligus, sehingga 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 yang dipenuhi promise) dalam urutan yang sama seperti 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 jaringan, pemuatannya dapat berlangsung beberapa detik lebih cepat daripada pemuatan satu per satu, dan lebih sedikit kode daripada percobaan pertama kita. Bab dapat didownload dalam urutan apa pun, tetapi muncul di layar dalam urutan yang tepat.

Namun, kita masih dapat memperbaiki performa yang dirasakan. Ketika bab satu masuk, kita harus menambahkannya ke laman. Hal ini memungkinkan pengguna mulai membaca sebelum bab lainnya 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.

Untuk melakukannya, ambil JSON untuk semua segmen secara bersamaan, lalu buat 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 pertama konten lebih cepat.

Dalam contoh sederhana ini, semua bab masuk kurang lebih pada waktu yang sama, tetapi manfaat menampilkannya satu per satu akan menjadi berlebihan pada bab yang lebih banyak dan lebih besar.

Melakukan langkah di atas dengan callback atau peristiwa gaya Node.js akan menggandakan kode, tetapi yang lebih penting lagi tidak mudah diikuti. Namun, ini bukan akhir cerita untuk promise, jika digabungkan dengan fitur ES6 lainnya akan menjadi lebih mudah.

Putaran bonus: kemampuan yang diperluas

Sejak awalnya saya menulis artikel ini, kemampuan untuk menggunakan Promise telah berkembang pesat. Sejak Chrome 55, fungsi asinkron telah memungkinkan kode berbasis promise untuk ditulis seolah-olah bersifat sinkron, tetapi tanpa memblokir thread utama. Anda dapat membaca hal ini lebih lanjut di my async functions article. Terdapat dukungan yang luas untuk Promise dan fungsi asinkron di beberapa browser utama. Anda dapat menemukan detailnya dalam referensi Promise dan fungsi asinkron MDN.

Terima kasih banyak kepada Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans, dan Yutaka Hirano yang telah memeriksa tata bahasa hal ini dan membuat koreksi/rekomendasi.

Terima kasih juga kepada Mathias Bynens yang telah memperbarui berbagai bagian dari artikel ini.