Promise JavaScript: pengantar

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

Jake Archibald
Jake Archibald

Pengembang, 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 tahu apa yang sedang diramaikan lebih lanjut. Mungkin Anda bahkan tidak yakin apa itu "janji" alamat IP internalnya. Anda akan mengangkat bahu, tapi berat kertas berkilauan membebani bahu Anda. Jika demikian, jangan mengkhawatirkannya, aku butuh waktu lama untuk mencari tahu mengapa aku harus peduli dengan hal ini barang. Sebaiknya Anda memulai dari awal.
  • Anda menendang 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? Anda mungkin ingin memulai dengan terminologi.
  • Anda sudah tahu tentang hal ini dan Anda mengejek mereka yang melompat dan turun ke bawah seolah-olah itu adalah berita. Luangkan waktu sejenak untuk mengeksplorasi keunggulan Anda sendiri, lalu langsung buka referensi API.

Dukungan browser dan polyfill

Dukungan Browser

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

Sumber

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

Ada apa sebenarnya?

JavaScript adalah thread tunggal, yang berarti bahwa dua bit skrip tidak dapat berjalan di pada waktu yang sama; harus dijalankan satu per satu. Di browser, JavaScript membagikan utas dengan banyak hal lain yang berbeda dari browser ke browser. Tapi biasanya JavaScript berada dalam antrean yang sama dengan menggambar, memperbarui gaya, dan penanganan tindakan pengguna (seperti menyorot teks dan interaksi 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, Anda dapat mengemudi dan melakukan percakapan pada saat yang sama. Satu-satunya pemblokiran yang harus kita hadapi adalah bersin, di mana semua aktivitas harus ditangguhkan selama bersin. Cukup menjengkelkan, terutama saat Anda mengemudi dan mencoba melakukan percakapan. Anda tidak perlu 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 gambarnya, menambahkan beberapa pemroses, lalu JavaScript dapat menghentikan eksekusi hingga salah satu pemroses tersebut dipanggil.

Sayangnya, dalam contoh di atas, ada kemungkinan peristiwa yang terjadi terjadi sebelum kita mulai mendengarkannya, jadi kita perlu mengatasinya menggunakan “kelengkapan” properti 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
});

Ini tidak menangkap gambar yang {i> error<i} sebelum kita sempat mendengarkan them; sayangnya DOM tidak memberi kita cara untuk melakukannya. Ini adalah memuat satu gambar. Hal-hal menjadi lebih kompleks jika kita ingin tahu kapan suatu gambar telah dimuat.

Peristiwa tidak selalu menjadi cara terbaik

Acara cocok untuk hal-hal yang dapat terjadi beberapa kali secara bersamaan objek—keyup, touchstart, dll. Dengan peristiwa tersebut, Anda tidak terlalu peduli tentang apa yang terjadi sebelum Anda memasang pemroses. Namun, apabila terkait dengan async berhasil/gagal, idealnya Anda menginginkan hal 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 "siap" yang menampilkan promise, kita bisa 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. Sistem ini tidak bisa berhasil atau gagal dua kali, dan tidak dapat beralih dari keberhasilan ke kegagalan atau sebaliknya.
  • Jika promise berhasil atau gagal, lalu Anda menambahkan keberhasilan/kegagalan , callback yang benar akan dipanggil, meskipun peristiwa itu mengambil sebelumnya.

Ini sangat berguna untuk keberhasilan/kegagalan asinkron, karena Anda tertarik dengan waktu persis sesuatu tersedia, dan lebih tertarik untuk bereaksi terhadap hasil proyek.

Terminologi promise

Domenic Denicola melakukan pemeriksaan terhadap draf pertama artikel ini dan memberi nilai "F" untuk terminologi. Dia memasukkan saya ke dalam tahanan, memaksa saya untuk menyalin Status dan Nasib 100 kali, dan menulis surat yang khawatir kepada orang tua saya. Meskipun begitu, saya masih banyak terminologi yang tercampur, tetapi berikut ini adalah 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, yang memiliki metode then. Istilah ini mengingatkan saya pada mantan Football Inggris 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 disebut Promises/A+. Jika Anda pengguna jQuery, mereka memiliki sesuatu yang mirip yang disebut Ditangguhkan. Namun, Item yang ditangguhkan 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 terstandardisasi, keseluruhan API akan 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, selesaikan dan tolak. Lakukan sesuatu dalam callback, mungkin asinkron, lalu panggil menyelesaikan jika semuanya bekerja, atau memanggil {i>Deny<i}.

Seperti throw di JavaScript lama, hal ini umum, tetapi tidak diwajibkan, tolak dengan objek Error. Manfaat objek Error adalah menangkap objek pelacakan tumpukan, sehingga alat 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 kegagalan. Keduanya bersifat opsional, sehingga Anda dapat menambahkan callback untuk hanya kasus berhasil atau gagal.

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

Meski fitur JavaScript, DOM tidak takut untuk menggunakannya. Di beberapa faktanya, semua DOM API baru dengan metode berhasil/gagal asinkron akan menggunakan promise. Ini sudah terjadi dengan Pengelolaan Kuota, Peristiwa Pemuatan Font, ServiceWorker, MIDI Web, Streaming, dan lainnya.

Kompatibilitas dengan library lain

JavaScript promise API akan memperlakukan apa pun dengan metode then() sebagai mirip promise (atau thenable dalam menghela napas), jadi jika Anda menggunakan library yang mengembalikan janji Q, tidak apa-apa, itu akan cocok dengan yang Promise JavaScript.

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, kadang ditangguhkan meneruskan beberapa argumen ke callback mereka, 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 ini yang biasanya Anda inginkan, atau setidaknya memberi Anda akses ke mendapatkan apa yang Anda inginkan. Juga, perlu diketahui bahwa jQuery tidak mengikuti konvensi meneruskan 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 ingin untuk menghentikan indikator lingkaran berputar pada saat itu juga, jika tidak akan terus berputar, ambil pusing, lalu masuk ke beberapa UI lain.

Tentu saja, Anda tidak akan menggunakan JavaScript untuk menyampaikan cerita, ditampilkan sebagai HTML lebih cepat, tetapi pola ini cukup umum saat menangani API: Banyak data mengambil, lalu melakukan sesuatu setelah semuanya selesai.

Untuk memulai, mari kita tangani pengambilan data dari jaringan:

Mem-janjikan XMLHttpRequest

API lama akan diupdate untuk menggunakan promise, jika memungkinkan yang kompatibel. 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 bagus, karena Semakin sedikit saya melihat unta kutub yang menyebalkan dari XMLHttpRequest, semakin bahagia hidup saya.

Perantaian

then() bukanlah akhir 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. Rab dapat mengubah fungsi {i>get<i} untuk menggunakan JSON, responseType, tapi kita juga bisa menyelesaikannya di 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 bisa membuat {i>shortcut<i}:

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 responsnya 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 set URL yang diminta, lalu kami akan meminta URL yang pertama. Pada saat inilah promise 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 sampai getChapter dipanggil, tetapi berikutnya waktu getChapter disebut kita akan menggunakan kembali promise cerita, jadi 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 memenuhi dan menolak, dalam kata-kata yang dijanjikan):

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 istimewa dari catch(), ini hanya gula untuk then(undefined, func), tetapi lebih mudah dibaca. Perhatikan bahwa kedua kode contoh di atas tidak berperilaku sama, contoh 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 melewati maju ke then() berikutnya dengan callback penolakan (atau catch(), karena setara). Dengan then(func1, func2), func1 atau func2 akan menjadi disebut, tidak pernah keduanya. Namun dengan then(func1).catch(func2), keduanya akan dipanggil jika func1 menolak, karena peristiwa tersebut adalah langkah terpisah dalam rantai. Naik 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 {i> try/catch<i} JavaScript normal, kesalahan yang yang terjadi dalam "try" langsung ke blok catch(). Berikut di atas sebagai diagram alur (karena saya menyukai diagram alur):

Ikuti garis biru untuk janji yang terpenuhi, atau merah untuk janji yang tolak.

Pengecualian dan promise JavaScript

Penolakan terjadi ketika 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);
})

Hal ini berarti berguna untuk melakukan semua pekerjaan yang terkait dengan promise di dalam callback konstruktor, jadi kesalahan secara 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), perintah itu akan melewati semua callback keberhasilan, yang mencakup satu getJSON() yang mencoba mengurai respons sebagai JSON, dan juga melewati yang menambahkan bab1.html ke laman tersebut. Sebaliknya, gambar tersebut bergerak ke tangkapan layar . Akibatnya, "Gagal menampilkan bab" akan ditambahkan ke halaman jika tindakan sebelumnya gagal.

Seperti try/catch JavaScript, error akan ditangkap dan kode berikutnya berlanjut, sehingga indikator lingkaran berputar selalu tersembunyi, seperti yang kita inginkan. Tujuan di atas menjadi versi asinkron yang tidak memblokir:

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 pemulihan dari error. Untuk melakukannya, cukup tampilkan kembali error. Kita bisa melakukan ini dalam metode getJSON() kita:

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 buat hal tersebut terjadi.

Paralelisme dan pengurutan: mendapatkan yang terbaik dari keduanya

Memikirkan asinkron tidak mudah. Jika Anda merasa kesulitan untuk mendapatkan sasaran, cobalah menulis kode seolah-olah kode tersebut adalah 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. Tapi itu disinkronkan dan mengunci browser saat segala sesuatunya diunduh. Kepada membuat pekerjaan ini menjadi 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 &amp; 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? 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 berbasis asinkron, jadi bab kita akan muncul dalam urutan apa pun yang mereka unduh, yang pada dasarnya adalah cara menulis {i>Pulp Fiksi<i}. Ini bukanlah Pulp Fiksi, jadi ayo 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 yang dijanjikan menjadi nilai apa pun yang Anda berikan. Jika Anda meneruskan instance Promise hanya akan menampilkannya (catatan: ini adalah diubah ke spesifikasi yang belum diikuti oleh beberapa implementasi). Jika Anda meneruskannya sesuatu seperti promise (memiliki metode then()), hal ini akan membuat Promise asli yang memenuhi/menolak dengan cara yang sama. Jika Anda lulus dalam nilai lain, misalnya, Promise.resolve('Hello'), fungsi ini membuat yang memenuhi janji dengan nilai tersebut. Jika Anda memanggilnya tanpa nilai, seperti di atas, memenuhinya dengan "undefined".

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

Kita bisa 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 "urutan" variabel. Callback reduksi dipanggil untuk setiap item dalam array. "urutan" adalah Promise.resolve() pertama kali, tetapi untuk sisa periode memanggil "urut" adalah nilai yang kita kembalikan dari panggilan sebelumnya. array.reduce sangat berguna untuk meringkas array menjadi satu nilai, adalah sebuah janji.

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

{i>Browser<i} cukup bagus dalam mengunduh banyak hal sekaligus, jadi kita kehilangan performa dengan mendownload bab satu per satu. Yang ingin kita lakukan adalah mengunduh semuanya sekaligus, lalu memprosesnya saat semuanya tiba. Untungnya ada API untuk ini:

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

Promise.all menggunakan array promise dan membuat promise yang akan memenuhi ketika semuanya berhasil diselesaikan. Anda mendapatkan serangkaian hasil (apa pun semua janji yang dijanjikan) dalam urutan yang sama dengan janji yang Anda berikan.

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

Tergantung pada koneksinya, waktu yang tersedia bisa lebih cepat daripada memuat satu per satu, dan lebih sedikit kode daripada percobaan pertama kita. Segmen dapat didownload dalam bentuk apa pun berurutan, tetapi mereka muncul di layar dalam urutan yang benar.

Namun, kita masih dapat memperbaiki performa yang dirasakan. Ketika bab satu tiba, kita harus menambahkannya ke halaman. Hal ini memungkinkan pengguna mulai membaca sebelum seluruh bab telah tiba. Jika bab tiga tiba, kita tidak akan menambahkannya ke karena pengguna mungkin tidak menyadari bab dua tidak ada. Ketika bab dua sudah tiba, kita dapat menambahkan bab dua dan tiga, dst.

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

Dalam contoh kecil ini, semua bab tiba kurang lebih pada waktu yang sama, tetapi manfaat menampilkan satu per satu akan menjadi berlebihan dengan bab.

Melakukan hal di atas dengan callback bergaya Node.js atau acara ada di sekitar menggandakan kode, tetapi yang lebih penting adalah tidak mudah diikuti. Namun, bukanlah akhir dari cerita untuk promise, jika digabungkan dengan fitur ES6 lainnya mereka akan menjadi lebih mudah.

Putaran bonus: kemampuan yang diperluas

Sejak awalnya saya menulis artikel ini, kemampuan untuk menggunakan Promise telah berkembang secara signifikan. Sejak Chrome 55, fungsi asinkron telah memungkinkan kode berbasis promise ditulis seolah-olah itu sinkron, tetapi tanpa memblokir thread utama. Anda dapat baca selengkapnya tentang hal tersebut di artikel fungsi asinkron saya. Ada dukungan yang luas untuk Promise dan fungsi asinkron di beberapa browser utama. Anda dapat menemukan detailnya di Promise dan asinkron alamat IP internal.

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 dan membuat koreksi/rekomendasi.

Terima kasih kepada Mathias Bynens atas memperbarui berbagai bagian dalam artikel.