→ Untuk penjelasan yang lebih umum tentang perilaku async dengan contoh berbeda, silakan lihat Mengapa variabel saya tidak berubah setelah saya memodifikasinya di dalam suatu fungsi? - Referensi kode tidak sinkron
→ Jika Anda sudah memahami masalahnya, lewati ke solusi yang mungkin di bawah ini.
Masalah
The A di Ajax singkatan asynchronous . Itu berarti mengirim permintaan (atau lebih tepatnya menerima respons) dikeluarkan dari alur eksekusi normal. Dalam contoh Anda, $.ajax
kembali segera dan pernyataan berikutnya return result;
,, dijalankan sebelum fungsi yang Anda lewati sebagaisuccess
panggilan balik bahkan dipanggil.
Berikut ini analogi yang diharapkan membuat perbedaan antara aliran sinkron dan asinkron lebih jelas:
Sinkronis
Bayangkan Anda menelepon seorang teman dan memintanya mencari sesuatu untuk Anda. Meskipun mungkin perlu waktu, Anda menunggu di telepon dan menatap ke angkasa, sampai teman Anda memberi Anda jawaban yang Anda butuhkan.
Hal yang sama terjadi ketika Anda membuat panggilan fungsi yang berisi kode "normal":
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Meskipun findItem
mungkin membutuhkan waktu lama untuk dieksekusi, kode apa pun yang muncul var item = findItem();
harus menunggu sampai fungsi mengembalikan hasilnya.
Tidak sinkron
Anda menelepon teman Anda lagi untuk alasan yang sama. Tetapi kali ini Anda memberi tahu dia bahwa Anda sedang terburu-buru dan dia harus menelepon Anda kembali di ponsel Anda. Anda menutup telepon, meninggalkan rumah dan melakukan apa pun yang Anda rencanakan. Setelah teman Anda menelepon Anda kembali, Anda berurusan dengan informasi yang dia berikan kepada Anda.
Itulah tepatnya yang terjadi ketika Anda melakukan permintaan Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Alih-alih menunggu respons, eksekusi berlanjut segera dan pernyataan setelah panggilan Ajax dieksekusi. Untuk mendapatkan respons pada akhirnya, Anda menyediakan fungsi untuk dipanggil setelah respons diterima, panggilan balik (perhatikan sesuatu? Panggilan balik ?). Pernyataan apa pun yang datang setelah panggilan itu dieksekusi sebelum panggilan balik dipanggil.
Solusi)
Merangkul sifat asinkron dari JavaScript!Sementara operasi asinkron tertentu menyediakan mitra sinkron (demikian juga "Ajax"), umumnya tidak disarankan untuk menggunakannya, terutama dalam konteks browser.
Mengapa ini buruk menurut Anda?
JavaScript berjalan di utas UI peramban dan proses yang berjalan lama akan mengunci UI, membuatnya tidak responsif. Selain itu, ada batas atas pada waktu eksekusi untuk JavaScript dan browser akan menanyakan kepada pengguna apakah akan melanjutkan eksekusi atau tidak.
Semua ini benar-benar pengalaman pengguna yang buruk. Pengguna tidak akan dapat mengetahui apakah semuanya berfungsi dengan baik atau tidak. Selanjutnya, efeknya akan lebih buruk bagi pengguna dengan koneksi yang lambat.
Berikut ini kita akan melihat tiga solusi berbeda yang semuanya membangun di atas satu sama lain:
- Janji dengan
async/await
(ES2017 +, tersedia di browser lama jika Anda menggunakan transpiler atau regenerator)
- Callback (populer di simpul)
- Janji dengan
then()
(ES2015 +, tersedia di browser lama jika Anda menggunakan salah satu dari banyak perpustakaan janji)
Ketiganya tersedia di browser saat ini, dan node 7+.
ES2017 +: Janji dengan async/await
Versi ECMAScript yang dirilis pada 2017 memperkenalkan dukungan level sintaksis untuk fungsi-fungsi asinkron. Dengan bantuan async
dan await
, Anda dapat menulis asinkron dalam "gaya sinkron". Kode masih asinkron, tetapi lebih mudah dibaca / dimengerti.
async/await
dibangun di atas janji: suatu async
fungsi selalu mengembalikan janji. await
"membuka" janji dan entah menghasilkan nilai yang dijanjikan dengan janji itu atau menimbulkan kesalahan jika janji itu ditolak.
Penting: Anda hanya dapat menggunakan await
di dalam suatu async
fungsi. Saat ini, tingkat atas await
belum didukung, jadi Anda mungkin harus membuat IIFE async ( Ekspresi Fungsi yang Segera Diminta ) untuk memulaiasync
konteks.
Anda dapat membaca lebih lanjut tentang async
dan await
di MDN.
Berikut adalah contoh yang dibangun di atas penundaan di atas:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
Saat peramban dan simpul versi mendukung async/await
. Anda juga dapat mendukung lingkungan yang lebih lama dengan mengubah kode Anda ke ES5 dengan bantuan regenerator (atau alat yang menggunakan regenerator, seperti Babel ).
Biarkan fungsi menerima panggilan balik
Panggilan balik hanyalah sebuah fungsi yang diteruskan ke fungsi lain. Fungsi lain itu dapat memanggil fungsi yang dilewati setiap kali siap. Dalam konteks proses asinkron, callback akan dipanggil setiap kali proses asinkron dilakukan. Biasanya, hasilnya diteruskan ke panggilan balik.
Dalam contoh pertanyaan, Anda dapat foo
menerima panggilan balik dan menggunakannya sebagai success
panggilan balik. Jadi ini
var result = foo();
// Code that depends on 'result'
menjadi
foo(function(result) {
// Code that depends on 'result'
});
Di sini kita mendefinisikan fungsi "inline" tetapi Anda dapat melewati semua referensi fungsi:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
sendiri didefinisikan sebagai berikut:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
akan merujuk ke fungsi yang kita lewati foo
ketika kita menyebutnya dan kita hanya meneruskannya ke success
. Yaitu setelah permintaan Ajax berhasil, $.ajax
akan memanggil callback
dan meneruskan tanggapan ke panggilan balik (yang dapat dirujuk denganresult
, karena ini adalah bagaimana kami mendefinisikan callback).
Anda juga dapat memproses respons sebelum meneruskannya ke callback:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
Lebih mudah menulis kode menggunakan panggilan balik daripada yang terlihat. Lagipula, JavaScript di browser sangat didorong oleh peristiwa (peristiwa DOM). Menerima respons Ajax tidak lain adalah suatu peristiwa.
Kesulitan dapat muncul ketika Anda harus bekerja dengan kode pihak ketiga, tetapi sebagian besar masalah dapat diselesaikan dengan hanya memikirkan alur aplikasi.
ES2015 +: Janji dengan itu ()
The Promise API adalah fitur baru dari ECMAScript 6 (ES2015), tetapi memiliki baik dukungan browser sudah. Ada juga banyak perpustakaan yang mengimplementasikan standar API Janji dan menyediakan metode tambahan untuk memudahkan penggunaan dan komposisi fungsi asinkron (mis. Bluebird ).
Janji adalah wadah untuk nilai masa depan . Ketika janji menerima nilai ( diselesaikan ) atau dibatalkan ( ditolak ), ia memberi tahu semua "pendengar" yang ingin mengakses nilai ini.
Keuntungan daripada panggilan balik biasa adalah mereka memungkinkan Anda untuk memisahkan kode Anda dan mereka lebih mudah untuk menulis.
Berikut adalah contoh sederhana menggunakan janji:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Diterapkan ke panggilan Ajax kami, kami bisa menggunakan janji seperti ini:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
Menjelaskan semua keuntungan yang dijanjikan menawarkan di luar cakupan jawaban ini, tetapi jika Anda menulis kode baru, Anda harus mempertimbangkannya dengan serius. Mereka memberikan abstraksi dan pemisahan kode Anda.
Informasi lebih lanjut tentang janji: HTML5 rocks - JavaScript Promises
Catatan: objek yang ditangguhkan jQuery
Objek yang ditangguhkan adalah implementasi kustom janji-janji jQuery (sebelum API Janji distandarisasi). Mereka berperilaku hampir seperti janji tetapi mengekspos API yang sedikit berbeda.
Setiap metode Ajax dari jQuery sudah mengembalikan "objek yang ditangguhkan" (sebenarnya janji objek yang ditangguhkan) yang bisa Anda kembalikan dari fungsi Anda:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Catatan: Janji Gotcha
Perlu diingat bahwa janji dan objek yang ditangguhkan hanyalah wadah untuk nilai masa depan, mereka bukan nilai itu sendiri. Misalnya, anggap Anda memiliki yang berikut:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
Kode ini salah mengerti masalah asinkron di atas. Secara khusus, $.ajax()
tidak membekukan kode saat memeriksa halaman '/ kata sandi' di server Anda - ia mengirim permintaan ke server dan saat menunggu, ia segera mengembalikan objek jQuery Ajax Deferred, bukan respons dari server. Itu berarti if
pernyataan akan selalu mendapatkan objek Ditangguhkan ini, perlakukan sebagai true
, dan lanjutkan seolah-olah pengguna masuk. Tidak bagus.
Namun perbaikannya mudah:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
Tidak disarankan: Panggilan "Ajax" yang disinkronkan
Seperti yang saya sebutkan, beberapa operasi asinkron (!) Memiliki mitra sinkron. Saya tidak menganjurkan penggunaannya, tetapi demi kelengkapan, berikut adalah cara Anda melakukan panggilan sinkron:
Tanpa jQuery
Jika Anda langsung menggunakan XMLHTTPRequest
objek, berikan false
argumen ketiga .open
.
jQuery
Jika Anda menggunakan jQuery , Anda dapat mengatur async
opsi ke false
. Perhatikan bahwa opsi ini sudah tidak digunakan lagi sejak jQuery 1.8. Anda kemudian dapat masih menggunakan success
panggilan balik atau mengakses responseText
properti objek jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Jika Anda menggunakan metode jQuery Ajax lainnya, seperti $.get
, $.getJSON
, dll, Anda harus mengubahnya ke $.ajax
(karena Anda hanya bisa memberikan parameter konfigurasi untuk $.ajax
).
Kepala! Tidak mungkin untuk membuat permintaan JSONP sinkron . JSONP pada dasarnya selalu asinkron (satu alasan lagi untuk tidak mempertimbangkan opsi ini).