Promise JavaScript: pengantar

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

Jake Archibald
Jake Archibald

Developer, bersiaplah untuk momen penting dalam sejarah pengembangan web.

[Drumroll begins]

Promise telah hadir di JavaScript.

[Kembang api meletus, kertas berkilau berjatuhan dari atas, kerumunan bersorak riuh]

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

  • Orang-orang bersorak di sekitar Anda, tetapi Anda tidak yakin apa yang membuat mereka bersemangat. Mungkin Anda bahkan tidak yakin apa itu "janji". Anda akan mengangkat bahu, tetapi berat kertas berkilau membebani bahu Anda. Jika ya, jangan khawatir, saya butuh waktu lama untuk mengetahui mengapa saya harus peduli dengan hal ini. Sebaiknya Anda mulai dari awal.
  • Anda meninju udara! 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 mulai dengan terminologi.
  • Anda sudah tahu tentang hal ini dan Anda mencibir orang-orang yang terkejut seolah-olah ini adalah berita baru bagi mereka. Luangkan waktu untuk menikmati keunggulan Anda sendiri, lalu langsung buka referensi API.

Dukungan browser dan polyfill

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

Untuk membuat browser yang tidak memiliki penerapan promises lengkap sesuai dengan kepatuhan spesifikasi, atau menambahkan promises ke browser dan Node.js lainnya, lihat polyfill (2k gzipped).

Apa yang membuat heboh?

JavaScript adalah thread tunggal, yang berarti bahwa dua bagian skrip tidak dapat berjalan secara bersamaan; keduanya harus berjalan satu per satu. Di browser, JavaScript berbagi thread dengan banyak hal lain yang berbeda dari 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 di salah satu hal ini akan menunda hal lainnya.

Sebagai manusia, Anda memiliki banyak kemampuan. Anda dapat mengetik dengan beberapa jari, Anda dapat mengemudi dan melakukan percakapan pada saat yang bersamaan. Satu-satunya fungsi pemblokiran yang harus kita tangani adalah bersin, yang mengharuskan semua aktivitas saat ini ditangguhkan selama bersin. Hal ini cukup mengganggu, terutama saat Anda sedang mengemudi dan mencoba melakukan percakapan. Anda tidak ingin menulis kode yang membuat bersin.

Anda mungkin telah menggunakan peristiwa dan callback untuk mengatasi hal ini. Berikut adalah peristiwa:

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 membuat bersin. Kita mendapatkan gambar, menambahkan beberapa pemroses, lalu JavaScript dapat berhenti dieksekusi hingga salah satu pemroses tersebut dipanggil.

Sayangnya, dalam contoh di atas, peristiwa tersebut mungkin terjadi sebelum kita mulai memantaunya, jadi kita perlu menyiasatinya menggunakan properti "complete" gambar:

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
});

Hal ini tidak menangkap gambar yang mengalami error sebelum kita berkesempatan untuk memantaunya; sayangnya, DOM tidak memberi kita cara untuk melakukannya. Selain itu, ini memuat satu gambar. Situasinya akan menjadi lebih rumit jika kita ingin mengetahui kapan sekumpulan gambar telah dimuat.

Acara tidak selalu merupakan 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 Anda melampirkan pemroses. Namun, untuk keberhasilan/kegagalan asinkron, idealnya Anda menginginkan 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, tetapi dengan penamaan yang lebih baik. Jika elemen gambar HTML memiliki metode "siap" yang menampilkan promise, kita dapat melakukan hal berikut:

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 sedikit mirip dengan pemroses peristiwa, kecuali:

  • Promise hanya dapat berhasil atau gagal satu kali. Operasi ini tidak dapat berhasil atau gagal dua kali, dan tidak dapat beralih dari berhasil ke gagal atau sebaliknya.
  • Jika janji telah berhasil atau gagal dan Anda kemudian menambahkan callback keberhasilan/kegagalan, callback yang benar akan dipanggil, meskipun peristiwa terjadi sebelumnya.

Hal ini sangat berguna untuk keberhasilan/kegagalan asinkron, karena Anda tidak terlalu tertarik dengan waktu yang tepat saat sesuatu tersedia, dan lebih tertarik untuk bereaksi terhadap hasilnya.

Terminologi Promise

Domenic Denicola membaca draf pertama artikel ini dan memberi saya nilai "F" untuk terminologi. Dia memasukkan saya ke ruang tahanan, memaksa saya menyalin Negara dan Nasib 100 kali, dan menulis surat yang mengkhawatirkan kepada orang tua saya. Meskipun demikian, saya masih sering mencampuradukkan terminologinya, tetapi berikut adalah dasarnya:

Promise dapat berupa:

  • terpenuhi - Tindakan yang terkait dengan janji berhasil
  • ditolak - Tindakan terkait janji gagal
  • menunggu keputusan - Belum dipenuhi atau ditolak
  • diselesaikan - Telah dipenuhi 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 ada sejak lama dalam bentuk library, seperti:

Promise di atas dan JavaScript memiliki perilaku umum dan standar yang disebut Promises/A+. Jika Anda adalah pengguna jQuery, ada sesuatu yang serupa yang disebut Deferreds. 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 Deferred dan memiliki masalah yang sama.

Meskipun implementasi promise mengikuti perilaku standar, API keseluruhannya berbeda. Promise JavaScript memiliki API yang serupa dengan RSVP.js. Berikut cara membuat janji:

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, yaitu callback dengan dua parameter, resolve dan reject. Lakukan sesuatu dalam callback, mungkin asinkron, lalu panggil resolve jika semuanya berfungsi, atau panggil reject.

Seperti throw di JavaScript lama, menolak dengan objek Error adalah hal yang biasa, tetapi tidak wajib. Manfaat objek Error adalah objek tersebut merekam stack trace, sehingga alat proses debug menjadi lebih bermanfaat.

Berikut cara menggunakan janji tersebut:

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() mengambil dua argumen, callback untuk kasus berhasil, dan callback lain untuk kasus gagal. Keduanya bersifat opsional, sehingga Anda dapat menambahkan callback hanya untuk kasus berhasil atau gagal.

Promise JavaScript dimulai di DOM sebagai "Futures", diganti namanya menjadi "Promises", dan akhirnya dipindahkan ke JavaScript. Memilikinya di JavaScript, bukan di DOM, sangat bagus karena akan tersedia di konteks JS non-browser seperti Node.js (apakah mereka menggunakannya di API inti mereka atau tidak adalah pertanyaan lain).

Meskipun merupakan fitur JavaScript, DOM tidak ragu untuk menggunakannya. Faktanya, semua DOM API baru dengan metode keberhasilan/kegagalan asinkron akan menggunakan promise. Hal ini sudah terjadi dengan Pengelolaan Kuota, Peristiwa Pemuatan Font, ServiceWorker, Web MIDI, Streams, dan lainnya.

Kompatibilitas dengan library lain

JavaScript Promises API 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 JavaScript Promises baru.

Meskipun, seperti yang saya sebutkan, Deferreds jQuery agak … tidak membantu. Untungnya, Anda dapat mentransmisikannya ke janji standar, yang sebaiknya 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) {
  // ...
})

Sementara itu, promise JS mengabaikan semua kecuali yang pertama:

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

Untungnya, biasanya inilah yang Anda inginkan, atau setidaknya memberi Anda akses ke apa yang Anda inginkan. Selain itu, perlu diketahui bahwa jQuery tidak mengikuti konvensi meneruskan objek Error ke penolakan.

Kode asinkron yang kompleks menjadi lebih mudah

Baiklah, mari kita coding beberapa hal. Misalnya, kita ingin:

  1. Mulai spinner untuk menunjukkan pemuatan
  2. Mengambil beberapa JSON untuk sebuah cerita, yang memberi kita judul, dan URL untuk setiap bab
  3. Menambahkan judul ke halaman
  4. Mengambil setiap bab
  5. Menambahkan cerita ke halaman
  6. Hentikan pemutar

… tetapi juga memberi tahu pengguna jika terjadi kesalahan di sepanjang proses. Kita juga ingin menghentikan spinner pada saat itu, jika tidak, spinner akan terus berputar, membuat pusing, dan bertabrakan dengan UI lain.

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

Untuk memulainya, mari kita bahas pengambilan data dari jaringan:

Mengubah XMLHttpRequest menjadi Promise

API lama akan diupdate untuk menggunakan promise, jika memungkinkan dengan cara yang kompatibel mundur. XMLHttpRequest adalah kandidat utama, tetapi sementara itu, 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, yang sangat bagus, karena makin sedikit saya melihat camel-casing XMLHttpRequest yang menjengkelkan, makin bahagia hidup saya.

Perantaian

then() bukan akhir dari cerita, Anda dapat menggabungkan then untuk mengubah nilai atau menjalankan tindakan asinkron tambahan satu demi satu.

Mengubah 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 adalah JSON, tetapi saat ini kami menerimanya sebagai teks biasa. Kita dapat mengubah fungsi get untuk menggunakan responseType JSON, tetapi kita juga dapat menyelesaikannya di promises land:

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

Karena JSON.parse() menggunakan satu argumen dan menampilkan nilai yang diubah, kita dapat membuat pintasan:

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

Sebenarnya, kita dapat membuat fungsi getJSON() dengan sangat mudah:

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

getJSON() masih menampilkan promise, yang mengambil URL lalu mengurai respons sebagai JSON.

Mengantrekan tindakan asinkron

Anda juga dapat menggabungkan then untuk menjalankan tindakan asinkron secara berurutan.

Saat Anda menampilkan sesuatu dari callback then(), hal itu sedikit 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 diselesaikan (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 serangkaian URL untuk diminta, lalu kita meminta URL pertama dari URL tersebut. Di sinilah janji benar-benar mulai menonjol dari pola callback sederhana.

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 menggunakan kembali promise story, sehingga story.json hanya diambil sekali. Yay Promises!

Penanganan error

Seperti yang kita lihat sebelumnya, then() mengambil dua argumen, satu untuk keberhasilan, satu untuk kegagalan (atau penuhi dan tolak, 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 hanyalah gula untuk then(undefined, func), tetapi lebih mudah dibaca. Perhatikan bahwa dua contoh kode di atas tidak berperilaku sama, 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 merupakan langkah terpisah dalam rantai. Lakukan 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" akan langsung masuk ke blok catch(). Berikut di atas sebagai diagram alur (karena saya menyukai diagram alur):

Ikuti garis biru untuk janji yang dipenuhi, atau garis merah untuk janji yang ditolak.

Pengecualian dan promise JavaScript

Penolakan terjadi saat promise ditolak secara eksplisit, tetapi juga secara implisit jika terjadi error 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 akan 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 offline), semua callback keberhasilan berikutnya akan dilewati, termasuk callback di getJSON() yang mencoba mengurai respons sebagai JSON, dan juga callback yang menambahkan chapter1.html ke halaman. Sebagai gantinya, kode akan berpindah ke callback catch. Akibatnya, "Gagal menampilkan bab" akan ditambahkan ke halaman jika salah satu tindakan sebelumnya gagal.

Seperti try/catch JavaScript, error akan ditangkap dan kode berikutnya akan berlanjut, sehingga spinner selalu disembunyikan, yang merupakan hal yang kita inginkan. Kode di atas menjadi versi asinkron non-blocking dari:

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'

Anda mungkin ingin melakukan catch() hanya untuk tujuan logging, tanpa memulihkan dari error. Untuk melakukannya, cukup lempar 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 wujudkan.

Paralelisme dan pengurutan: mendapatkan yang terbaik dari keduanya

Berpikir secara asinkron tidaklah mudah. Jika Anda kesulitan untuk memulai, coba tulis kode seolah-olah 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, sinkronisasi ini mengunci browser saat mendownload sesuatu. Agar dapat berfungsi secara asinkron, kita menggunakan then() untuk membuat berbagai hal terjadi satu demi 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 melakukan loop melalui URL bab 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 mendukung async, jadi bab kita akan muncul dalam urutan apa pun saat didownload, yang pada dasarnya seperti cara Pulp Fiction ditulis. Ini bukan Pulp Fiction, jadi mari kita perbaiki.

Membuat urutan

Kita ingin mengubah array chapterUrls menjadi urutan janji. 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 janji yang diselesaikan 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 mirip promise (memiliki metode then()), maka Promise yang sebenarnya akan dibuat dan dipenuhi/ditolak dengan cara yang sama. Jika Anda meneruskan nilai lain, misalnya, Promise.resolve('Hello'), kode ini membuat promise yang dipenuhi dengan nilai tersebut. Jika Anda memanggilnya tanpa nilai, seperti di atas, nilai tersebut akan diisi dengan "undefined".

Ada juga Promise.reject(val), yang membuat promise yang ditolak dengan nilai yang Anda berikan (atau tidak ditentukan).

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())

Hal ini sama dengan contoh sebelumnya, tetapi tidak memerlukan variabel "sequence" yang terpisah. Callback reduce dipanggil untuk setiap item dalam array. "sequence" adalah Promise.resolve() pada panggilan pertama, tetapi untuk panggilan lainnya, "sequence" adalah apa pun yang kita tampilkan dari panggilan sebelumnya. array.reduce sangat berguna untuk menyederhanakan array menjadi satu nilai, yang dalam kasus ini adalah promise.

Mari kita gabungkan 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 inilah dia, versi sinkronisasi yang sepenuhnya asinkron. Namun, kita bisa melakukan yang lebih baik. Saat ini, halaman kita sedang didownload seperti ini:

Browser cukup bagus dalam mendownload beberapa hal sekaligus, jadi kita kehilangan performa dengan mendownload bab satu demi satu. Yang ingin kita lakukan adalah mendownload semuanya secara bersamaan, lalu memprosesnya setelah semuanya tiba. Untungnya, ada API untuk hal ini:

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

Promise.all mengambil array promise dan membuat promise yang terpenuhi saat semuanya berhasil diselesaikan. Anda akan mendapatkan array hasil (apa pun yang dipenuhi oleh promise) 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, hal ini bisa beberapa detik lebih cepat daripada memuat satu per satu, dan kodenya lebih sedikit daripada percobaan pertama kita. Segmen dapat didownload dalam urutan apa pun, tetapi akan muncul di layar dalam urutan yang benar.

Namun, kita masih dapat meningkatkan performa yang dirasakan. Saat bab satu tiba, kita harus menambahkannya ke halaman. Hal ini memungkinkan pengguna mulai membaca sebelum bagian bab lainnya tiba. Saat bab tiga tiba, kita tidak akan menambahkannya ke halaman karena pengguna mungkin tidak menyadari bahwa bab dua tidak ada. Saat bab dua tersedia, kita dapat menambahkan bab dua dan tiga, dan seterusnya.

Untuk melakukannya, kita mengambil JSON untuk semua bab secara bersamaan, 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 inilah hasilnya, yang terbaik dari keduanya. Waktu yang diperlukan untuk mengirimkan semua konten sama, tetapi pengguna mendapatkan bagian pertama konten lebih cepat.

Dalam contoh sederhana ini, semua bab tiba pada waktu yang hampir bersamaan, tetapi manfaat menampilkan satu per satu akan lebih terasa dengan bab yang lebih banyak dan lebih panjang.

Melakukan hal di atas dengan callback atau peristiwa gaya Node.js memerlukan kode dua kali lebih banyak, tetapi yang lebih penting, tidak semudah diikuti. Namun, ini bukan akhir dari cerita untuk promise, jika digabungkan dengan fitur ES6 lainnya, promise menjadi lebih mudah.

Babak bonus: kemampuan yang ditingkatkan

Sejak saya menulis artikel ini, kemampuan untuk menggunakan Promise telah berkembang pesat. Sejak Chrome 55, fungsi asinkron telah memungkinkan kode berbasis promise ditulis seolah-olah sinkron, tetapi tanpa memblokir thread utama. Anda dapat membaca selengkapnya tentang hal itu di artikel fungsi asinkron saya. Ada dukungan luas untuk Promise dan fungsi asinkron di browser utama. Anda dapat menemukan detailnya di referensi Promise dan fungsi async MDN.

Terima kasih banyak kepada Anne van Kesteren, Domenic Denicola, Tom Ashworth, Remy Sharp, Addy Osmani, Arthur Evans, dan Yutaka Hirano yang telah membaca draf ini dan memberikan koreksi/rekomendasi.

Selain itu, terima kasih kepada Mathias Bynens yang telah memperbarui berbagai bagian artikel ini.