Mengapa variabel saya tidak diubah setelah saya memodifikasinya di dalam fungsi? - Referensi kode asinkron
Dengan contoh berikut, mengapa tidak outerScopeVar
ditentukan dalam semua kasus?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
Mengapa itu menghasilkan undefined
semua contoh ini? Saya tidak ingin solusi, saya ingin tahu mengapa ini terjadi.
Catatan: Ini adalah pertanyaan kanonik untuk asinkronitas JavaScript . Jangan ragu untuk menyempurnakan pertanyaan ini dan menambahkan contoh yang lebih sederhana yang dapat diidentifikasi oleh komunitas.
Jawaban
Jawaban satu kata: asinkronitas .
Kata Pengantar
Topik ini telah diulang setidaknya beberapa ribu kali, di sini, di Stack Overflow. Oleh karena itu, pertama-tama saya ingin menunjukkan beberapa sumber yang sangat berguna:
@Felix Kling jawaban untuk "Bagaimana cara mengembalikan respon dari panggilan asynchronous?" . Lihat jawabannya yang luar biasa menjelaskan aliran sinkron dan asinkron, serta bagian "Menyusun ulang kode".
@Benjamin Gruenbaum juga berusaha keras menjelaskan asinkronitas di utas yang sama.Jawaban @Matt Esch untuk "Dapatkan data dari fs.readFile" juga menjelaskan asinkronitas dengan sangat baik dengan cara yang sederhana.
Jawaban atas pertanyaan yang ada
Mari kita telusuri perilaku umum terlebih dahulu. Dalam semua contoh, outerScopeVar
itu dimodifikasi di dalam suatu fungsi . Fungsi itu jelas tidak segera dijalankan, itu ditugaskan atau diteruskan sebagai argumen. Itulah yang kami sebut callback .
Sekarang pertanyaannya adalah, kapan panggilan balik itu dipanggil?
Tergantung kasusnya. Mari kita coba menelusuri beberapa perilaku umum lagi:
img.onload
dapat dipanggil suatu saat nanti , ketika (dan jika) gambar telah berhasil dimuat.setTimeout
dapat dipanggil suatu saat nanti , setelah penundaan telah kedaluwarsa dan batas waktu belum dibatalkan olehclearTimeout
. Catatan: bahkan saat menggunakan0
sebagai penundaan, semua browser memiliki batas penundaan waktu tunggu minimum (ditetapkan menjadi 4 md dalam spesifikasi HTML5).$.post
Callback jQuery dapat dipanggil suatu saat nanti , ketika (dan jika) permintaan Ajax telah berhasil diselesaikan.- Node.js
fs.readFile
mungkin dipanggil suatu saat nanti , ketika file telah berhasil dibaca atau terjadi kesalahan.
Dalam semua kasus, kami memiliki panggilan balik yang mungkin berjalan di masa mendatang . Ini "suatu saat di masa mendatang" adalah apa yang kami sebut sebagai aliran asinkron .
Eksekusi asinkron didorong keluar dari aliran sinkron. Artinya, kode asinkron tidak akan pernah dijalankan saat tumpukan kode sinkron sedang dijalankan. Ini adalah arti JavaScript menjadi single-threaded.
Lebih khusus lagi, ketika mesin JS dalam keadaan diam - tidak mengeksekusi tumpukan (a) kode sinkron - itu akan mengumpulkan peristiwa yang mungkin telah memicu panggilan balik asinkron (mis. Batas waktu kedaluwarsa, respons jaringan yang diterima) dan mengeksekusinya satu demi satu. Ini dianggap sebagai Event Loop .
Artinya, kode asinkron yang disorot dalam bentuk merah yang digambar dengan tangan hanya dapat dijalankan setelah semua kode sinkron yang tersisa di blok kode masing-masing telah dijalankan:
Singkatnya, fungsi callback dibuat secara sinkron tetapi dijalankan secara asinkron. Anda hanya tidak bisa mengandalkan eksekusi fungsi asinkron sampai Anda tahu itu telah dijalankan, dan bagaimana melakukannya?
Ini sederhana, sungguh. Logika yang bergantung pada eksekusi fungsi asinkron harus dimulai / dipanggil dari dalam fungsi asinkron ini. Misalnya, memindahkan alert
s dan console.log
s terlalu di dalam fungsi callback akan mengeluarkan hasil yang diharapkan, karena hasilnya tersedia pada saat itu.
Menerapkan logika panggilan balik Anda sendiri
Seringkali Anda perlu melakukan lebih banyak hal dengan hasil dari fungsi asinkron atau melakukan hal yang berbeda dengan hasil bergantung di mana fungsi asinkron dipanggil. Mari kita bahas contoh yang lebih kompleks:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Catatan: Saya menggunakan setTimeout
dengan penundaan acak sebagai fungsi asinkron generik, contoh yang sama berlaku untuk Ajax readFile
,, onload
dan aliran asinkron lainnya.
Contoh ini jelas mengalami masalah yang sama seperti contoh lainnya, tidak menunggu hingga fungsi asynchronous dijalankan.
Mari kita atasi itu dengan menerapkan sistem panggilan balik kita sendiri. Pertama, kita singkirkan yang jelek outerScopeVar
yang sama sekali tidak berguna dalam kasus ini. Kemudian kami menambahkan parameter yang menerima argumen fungsi, callback kami. Ketika operasi asynchronous selesai, kita memanggil callback ini dengan meneruskan hasilnya. Implementasinya (harap baca komentar secara berurutan):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Cuplikan kode dari contoh di atas:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Paling sering dalam kasus penggunaan nyata, DOM API dan sebagian besar perpustakaan sudah menyediakan fungsionalitas panggilan balik ( helloCatAsync
implementasi dalam contoh demonstratif ini). Anda hanya perlu meneruskan fungsi callback dan memahami bahwa fungsi tersebut akan dijalankan dari aliran sinkron, dan menyusun ulang kode Anda untuk mengakomodasinya.
Anda juga akan melihat bahwa karena sifat asynchronous, tidak mungkin return
nilai dari aliran asinkron kembali ke aliran sinkron tempat callback didefinisikan, karena callback asinkron dijalankan lama setelah kode sinkron selesai dijalankan.
Alih-alih return
mengambil nilai dari callback asinkron, Anda harus menggunakan pola callback, atau ... Promises.
Janji
Meskipun ada cara untuk menahan panggilan balik dengan vanilla JS, promise semakin populer dan saat ini distandarisasi di ES6 (lihat Promise - MDN ).
Promises (alias Futures) memberikan pembacaan kode asynchronous yang lebih linier, dan dengan demikian menyenangkan, tetapi menjelaskan seluruh fungsinya berada di luar cakupan pertanyaan ini. Sebagai gantinya, saya akan meninggalkan sumber daya yang sangat baik ini untuk yang tertarik:
Lebih banyak bahan bacaan tentang asinkronitas JavaScript
- The Art of Node - Callbacks menjelaskan kode asinkron dan callback dengan sangat baik dengan contoh vanilla JS dan kode Node.js juga.
Catatan: Saya telah menandai jawaban ini sebagai Wiki Komunitas, maka siapa pun dengan setidaknya 100 reputasi dapat mengedit dan memperbaikinya! Silakan memperbaiki jawaban ini, atau kirimkan jawaban yang benar-benar baru jika Anda juga mau.
Saya ingin mengubah pertanyaan ini menjadi topik kanonik untuk menjawab masalah asinkronitas yang tidak terkait dengan Ajax (ada Bagaimana cara mengembalikan respons dari panggilan AJAX? Untuk itu), maka topik ini membutuhkan bantuan Anda untuk menjadi sebaik dan bermanfaat mungkin !
Jawaban Fabrício tepat; tetapi saya ingin melengkapi jawabannya dengan sesuatu yang tidak terlalu teknis, yang berfokus pada analogi untuk membantu menjelaskan konsep asinkronitas .
Sebuah Analogi ...
Kemarin, pekerjaan yang saya lakukan membutuhkan beberapa informasi dari seorang kolega. Saya menelepon dia; begini percakapannya:
Me : Hi Bob, saya perlu tahu bagaimana kita foo 'd bar ' d pekan lalu. Jim menginginkan laporannya, dan hanya Anda yang mengetahui detailnya.
Bob : Tentu, tapi itu akan memakan waktu sekitar 30 menit?
Saya : Itu bagus Bob. Beri saya cincin kembali ketika Anda mendapat informasi!
Pada titik ini, saya menutup telepon. Karena saya membutuhkan informasi dari Bob untuk melengkapi laporan saya, saya meninggalkan laporan dan pergi untuk minum kopi, kemudian saya membaca beberapa email. 40 menit kemudian (Bob lambat), Bob menelepon kembali dan memberi saya informasi yang saya butuhkan. Pada titik ini, saya melanjutkan pekerjaan saya dengan laporan saya, karena saya memiliki semua informasi yang saya butuhkan.
Bayangkan jika percakapan berjalan seperti ini;
Me : Hi Bob, saya perlu tahu bagaimana kita foo 'd bar ' d pekan lalu. Jim ingin laporannya, dan Anda satu-satunya yang mengetahui detailnya.
Bob : Tentu, tapi itu akan memakan waktu sekitar 30 menit?
Saya : Itu bagus Bob. Aku akan menunggu.
Dan saya duduk di sana dan menunggu. Dan menunggu. Dan menunggu. Selama 40 menit. Tidak melakukan apa-apa selain menunggu. Akhirnya, Bob memberi saya informasi, kami menutup telepon, dan saya menyelesaikan laporan saya. Tapi saya kehilangan produktivitas selama 40 menit.
Ini adalah perilaku asinkron vs. sinkron
Persis seperti inilah yang terjadi di semua contoh dalam pertanyaan kita. Memuat gambar, memuat file dari disk, dan meminta halaman melalui AJAX semuanya adalah operasi yang lambat (dalam konteks komputasi modern).
Daripada menunggu operasi lambat ini selesai, JavaScript memungkinkan Anda mendaftarkan fungsi callback yang akan dijalankan ketika operasi lambat telah selesai. Sementara itu, JavaScript akan terus mengeksekusi kode lain. Fakta bahwa JavaScript mengeksekusi kode lain sambil menunggu operasi yang lambat selesai membuat perilaku asinkron . Jika JavaScript menunggu hingga operasi selesai sebelum menjalankan kode lain, ini akan menjadi perilaku sinkron .
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
Pada kode di atas, kami meminta JavaScript untuk dimuat lolcat.png
, yang merupakan operasi sloooow . Fungsi panggilan balik akan dijalankan setelah operasi yang lambat ini selesai, tetapi sementara itu, JavaScript akan terus memproses baris kode berikutnya; mis alert(outerScopeVar)
.
Inilah mengapa kita melihat peringatan itu muncul undefined
; karena alert()
diproses segera, bukan setelah gambar dimuat.
Untuk memperbaiki kode kita, yang harus kita lakukan adalah memindahkan alert(outerScopeVar)
kode tersebut ke dalam fungsi panggilan balik. Sebagai akibatnya, kita tidak lagi membutuhkan outerScopeVar
variabel yang dideklarasikan sebagai variabel global.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Anda akan selalu melihat callback ditetapkan sebagai fungsi, karena itulah satu-satunya cara * dalam JavaScript untuk menentukan beberapa kode, tetapi tidak menjalankannya sampai nanti.
Oleh karena itu, dalam semua contoh kita, function() { /* Do something */ }
adalah callback; untuk memperbaiki semua contoh, yang harus kita lakukan adalah memindahkan kode yang memerlukan respons operasi ke sana!
* Secara teknis Anda bisa menggunakan eval()
juga, tetapi eval()
jahat untuk tujuan ini
Bagaimana cara membuat penelepon saya menunggu?
Saat ini Anda mungkin memiliki beberapa kode yang mirip dengan ini;
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Namun, sekarang kita tahu bahwa itu return outerScopeVar
terjadi dengan segera; sebelum onload
fungsi callback memperbarui variabel. Ini mengarah pada getWidthOfImage()
kembali undefined
, dan undefined
waspada.
Untuk memperbaikinya, kita perlu mengizinkan pemanggilan fungsi getWidthOfImage()
untuk mendaftarkan callback, kemudian memindahkan peringatan lebar ke dalam callback itu;
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... seperti sebelumnya, perhatikan bahwa kami telah dapat menghapus variabel global (dalam kasus ini width
).
Berikut adalah jawaban yang lebih ringkas untuk orang-orang yang mencari referensi cepat serta beberapa contoh menggunakan promise dan async / await.
Mulailah dengan pendekatan naif (yang tidak berhasil) untuk fungsi yang memanggil metode asinkron (dalam kasus ini setTimeout
) dan mengembalikan pesan:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
masuk dalam kasus ini karena getMessage
kembali sebelum setTimeout
callback dipanggil dan diperbarui outerScopeVar
.
Dua cara utama untuk mengatasinya menggunakan callback dan janji :
Panggilan balik
Perubahannya di sini adalah getMessage
menerima callback
parameter yang akan dipanggil untuk mengirimkan hasil kembali ke kode panggilan setelah tersedia.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Promises memberikan alternatif yang lebih fleksibel daripada callback karena dapat digabungkan secara alami untuk mengoordinasikan beberapa operasi asinkron. Sebuah Janji / A + penerapan standar native disediakan dalam node.js (0.12+) dan banyak browser saat ini, tetapi juga diterapkan di perpustakaan seperti Bluebird dan Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Ditunda
jQuery menyediakan fungsionalitas yang mirip dengan promise dengan Deferreds-nya.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
async / await
Jika lingkungan JavaScript Anda menyertakan dukungan untuk async
dan await
(seperti Node.js 7.6+), Anda dapat menggunakan promise secara sinkron dalam async
fungsi:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Untuk menyatakan yang sudah jelas, cangkir itu mewakili outerScopeVar
.
Fungsi asinkron menjadi seperti ...
Jawaban lainnya sangat bagus dan saya hanya ingin memberikan jawaban langsung untuk ini. Hanya membatasi ke panggilan asynchronous jQuery
Semua panggilan ajax (termasuk $.get
atau $.post
atau $.ajax
) bersifat asinkron.
Mempertimbangkan teladan Anda
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
Eksekusi kode dimulai dari baris 1, mendeklarasikan variabel dan pemicu serta panggilan asinkron di baris 2, (yaitu, permintaan posting) dan melanjutkan eksekusinya dari baris 3, tanpa menunggu permintaan posting menyelesaikan eksekusinya.
Katakanlah permintaan posting membutuhkan waktu 10 detik untuk menyelesaikan, nilai outerScopeVar
hanya akan ditetapkan setelah 10 detik itu.
Untuk mencoba,
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Sekarang ketika Anda menjalankan ini, Anda akan mendapatkan peringatan di baris 3. Sekarang tunggu beberapa saat sampai Anda yakin permintaan posting telah mengembalikan beberapa nilai. Kemudian ketika Anda mengklik OK, pada kotak peringatan, peringatan berikutnya akan mencetak nilai yang diharapkan, karena Anda menunggu.
Dalam skenario kehidupan nyata, kodenya menjadi,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Semua kode yang tergantung pada panggilan asynchronous, dipindahkan ke dalam blok asynchronous, atau dengan menunggu panggilan asynchronous.
Dalam semua skenario outerScopeVar
ini diubah atau diberi nilai secara asinkron atau terjadi di lain waktu (menunggu atau mendengarkan beberapa peristiwa terjadi), yang eksekusi saat ini tidak akan menunggu . Jadi semua kasus ini menghasilkan aliran eksekusi saat iniouterScopeVar = undefined
Mari kita bahas setiap contoh (saya menandai bagian yang disebut asynchronous atau tertunda untuk beberapa peristiwa terjadi):
1.
Disini kita mendaftarkan eventlistner yang akan dieksekusi pada event tersebut. Disini memuat gambar. Kemudian eksekusi saat ini berlanjut dengan baris berikutnya img.src = 'lolcat.png';
dan alert(outerScopeVar);
sementara kejadian tersebut mungkin tidak terjadi. yaitu, fungsi img.onload
menunggu gambar yang dimaksud dimuat, secara tidak terduga. Ini akan terjadi semua contoh berikut- acaranya mungkin berbeda.
2.
Di sini acara waktu tunggu memainkan peran, yang akan memanggil penangan setelah waktu yang ditentukan. Ini dia 0
, tetapi masih mendaftarkan peristiwa asinkron, itu akan ditambahkan ke posisi terakhir Event Queue
untuk eksekusi, yang membuat penundaan terjamin.
3.
4.
Node dapat dianggap sebagai raja pengkodean asinkron. Di sini fungsi yang ditandai terdaftar sebagai penangan panggilan balik yang akan dijalankan setelah membaca file yang ditentukan.
5.
Janji yang jelas (sesuatu akan dilakukan di masa depan) adalah asinkron. lihat Apa perbedaan antara Deferred, Promise dan Future di JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript