Mengapa variabel saya tidak diubah setelah saya memodifikasinya di dalam fungsi? - Referensi kode asinkron

May 15 2014

Dengan contoh berikut, mengapa tidak outerScopeVarditentukan 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 undefinedsemua 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

605 FabrícioMatté May 15 2014 at 06:55

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:


Jawaban atas pertanyaan yang ada

Mari kita telusuri perilaku umum terlebih dahulu. Dalam semua contoh, outerScopeVaritu 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.onloaddapat dipanggil suatu saat nanti , ketika (dan jika) gambar telah berhasil dimuat.
  • setTimeoutdapat dipanggil suatu saat nanti , setelah penundaan telah kedaluwarsa dan batas waktu belum dibatalkan oleh clearTimeout. Catatan: bahkan saat menggunakan 0sebagai penundaan, semua browser memiliki batas penundaan waktu tunggu minimum (ditetapkan menjadi 4 md dalam spesifikasi HTML5).
  • $.postCallback jQuery dapat dipanggil suatu saat nanti , ketika (dan jika) permintaan Ajax telah berhasil diselesaikan.
  • Node.js fs.readFilemungkin 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 alerts dan console.logs 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 setTimeoutdengan penundaan acak sebagai fungsi asinkron generik, contoh yang sama berlaku untuk Ajax readFile,, onloaddan 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 outerScopeVaryang 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 ( helloCatAsyncimplementasi 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 returnnilai dari aliran asinkron kembali ke aliran sinkron tempat callback didefinisikan, karena callback asinkron dijalankan lama setelah kode sinkron selesai dijalankan.

Alih-alih returnmengambil 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 !

158 Matt May 29 2014 at 16:09

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 outerScopeVarvariabel 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 outerScopeVarterjadi dengan segera; sebelum onloadfungsi callback memperbarui variabel. Ini mengarah pada getWidthOfImage()kembali undefined, dan undefinedwaspada.

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

75 JohnnyHK Jan 21 2015 at 06:42

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

undefinedmasuk dalam kasus ini karena getMessagekembali sebelum setTimeoutcallback dipanggil dan diperbarui outerScopeVar.

Dua cara utama untuk mengatasinya menggunakan callback dan janji :

Panggilan balik

Perubahannya di sini adalah getMessagemenerima callbackparameter 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);
});

Janji

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 asyncdan await(seperti Node.js 7.6+), Anda dapat menggunakan promise secara sinkron dalam asyncfungsi:

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();
58 JohannesFahrenkrug Dec 08 2015 at 23:48

Untuk menyatakan yang sudah jelas, cangkir itu mewakili outerScopeVar.

Fungsi asinkron menjadi seperti ...

14 Teja Feb 26 2016 at 10:59

Jawaban lainnya sangat bagus dan saya hanya ingin memberikan jawaban langsung untuk ini. Hanya membatasi ke panggilan asynchronous jQuery

Semua panggilan ajax (termasuk $.getatau $.postatau $.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 outerScopeVarhanya 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.

11 TomSebastian Oct 27 2015 at 13:35

Dalam semua skenario outerScopeVarini 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.onloadmenunggu 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 Queueuntuk eksekusi, yang membuat penundaan terjamin.

3.

Kali ini callback ajax.

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