Değişkenim bir işlevin içinde değiştirdikten sonra neden değişmiyor? - Eşzamansız kod referansı
Aşağıdaki örnekler göz önüne alındığında, neden outerScopeVar
her durumda tanımsızdır?
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);
Neden undefined
tüm bu örneklerde çıktı veriyor ? Geçici çözümler istemiyorum, bunun neden olduğunu bilmek istiyorum .
Not: Bu, JavaScript eşzamansızlığı için kanonik bir sorudur . Bu soruyu geliştirmekten ve topluluğun özdeşleşebileceği daha basitleştirilmiş örnekler eklemekten çekinmeyin.
Yanıtlar
Tek kelimelik cevap: eşzamansızlık .
Önsözler
Bu konu, burada Stack Overflow'da en az birkaç bin kez yinelenmiştir. Bu nedenle, ilk olarak son derece yararlı bazı kaynaklara işaret etmek istiyorum:
@Felix Kling'in "Eşzamansız bir aramadan gelen yanıtı nasıl geri getirebilirim?" . Eşzamanlı ve eşzamansız akışları açıklayan mükemmel cevabının yanı sıra "Kodu yeniden yapılandırma" bölümüne bakın.
@Benjamin Gruenbaum da aynı başlıkta eşzamansızlığı açıklamak için çok çaba sarf etti.@Matt Esch'in "fs.readFile'dan veri al" sorusuna yanıtı da asenkronizasyonu basit bir şekilde son derece iyi açıklıyor.
Eldeki sorunun cevabı
Önce ortak davranışı izleyelim. Tüm örneklerde, outerScopeVar
bir işlevin içinde değiştirilir . Bu işlev açıkça hemen çalıştırılmıyor, bir bağımsız değişken olarak atanıyor veya aktarılıyor. Biz buna geri arama diyoruz .
Şimdi soru şu, bu geri arama ne zaman aranıyor?
Davaya bağlıdır. Tekrar bazı yaygın davranışları izlemeye çalışalım:
img.onload
görüntünün başarıyla yüklendiğinde (ve eğer) gelecekte bir ara çağrılabilir .setTimeout
Gecikme süresi dolduktan ve zaman aşımı tarafından iptal edilmeden ileride çağrılabilirclearTimeout
. Not:0
Gecikme olarak kullanıldığında bile , tüm tarayıcıların minimum zaman aşımı gecikme sınırı vardır (HTML5 spesifikasyonunda 4 ms olarak belirtilmiştir).- jQuery'nin
$.post
geri araması , Ajax isteği başarıyla tamamlandığında (ve eğer) ileride çağrılabilir . - Node.js , gelecekte dosya başarıyla okunduğunda veya bir hata atıldığında
fs.readFile
çağrılabilir .
Her durumda, gelecekte çalışabilecek bir geri aramamız var . Bu "gelecekte bir zaman" bizim eşzamansız akış dediğimiz şeydir .
Eşzamansız yürütme, eşzamanlı akış dışına itilir. Yani, zaman uyumsuz kod , eşzamanlı kod yığını yürütülürken hiçbir zaman çalıştırılmayacaktır. JavaScript'in tek iş parçacıklı olmasının anlamı budur.
Daha spesifik olarak, JS motoru boştayken - bir (a) senkron kod yığını çalıştırmadığında - eşzamansız geri aramaları (örneğin süresi dolan zaman aşımı, alınan ağ yanıtı) tetikleyen olayları yoklayacak ve bunları birbiri ardına yürütecektir. Bu, Olay Döngüsü olarak kabul edilir .
Yani, elle çizilmiş kırmızı şekillerde vurgulanan eşzamansız kod, yalnızca kendi ilgili kod bloklarında kalan eşzamanlı kodun tamamı çalıştırıldıktan sonra çalıştırılabilir:
Kısacası, geri çağırma işlevleri eşzamanlı olarak oluşturulur ancak eşzamansız olarak yürütülür. Bir asenkron işlevin yürütüldüğünü bilene kadar yürütülmesine güvenemezsiniz ve bunu nasıl yaparsınız?
Gerçekten çok basit. Eşzamansız işlevin yürütülmesine bağlı olan mantık, bu eşzamansız işlevin içinden başlatılmalı / çağrılmalıdır. Örneğin, alert
s ve console.log
leri geri çağırma işlevinin içine taşımak beklenen sonucu verir, çünkü sonuç o noktada mevcuttur.
Kendi geri arama mantığınızı uygulama
Çoğu zaman, eşzamansız bir işlevin sonucuyla daha fazla şey yapmanız veya eşzamansız işlevin nerede çağrıldığına bağlı olarak sonuçla farklı şeyler yapmanız gerekir. Biraz daha karmaşık bir örneği ele alalım:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Not: Ben kullanıyorum setTimeout
genel asenkron fonksiyonu olarak rastgele bir gecikme ile, aynı örnek Ajax için geçerlidir readFile
, onload
ve diğer asenkron akışı.
Bu örnek açıkça diğer örneklerle aynı sorundan muzdariptir, asenkron işlevin yürütülmesini beklememektedir.
Kendimize ait bir geri arama sistemi uygulayarak bunun üstesinden gelelim. Öncelikle, outerScopeVar
bu durumda tamamen yararsız olan o çirkinlikten kurtuluyoruz . Sonra bir fonksiyon argümanını kabul eden bir parametre ekliyoruz, geri aramamız. Eşzamansız işlem bittiğinde, bu geri aramayı sonucu geçirerek çağırıyoruz. Uygulama (lütfen yorumları sırayla okuyun):
// 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);
}
Yukarıdaki örneğin kod pasajı:
// 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);
}
Çoğu zaman gerçek kullanım durumlarında, DOM API ve çoğu kitaplık, geri arama işlevini zaten sağlar ( helloCatAsync
bu örnek örnekteki uygulama). Yalnızca geri arama işlevini iletmeniz ve eşzamanlı akış dışında çalışacağını anlamanız ve buna uyum sağlamak için kodunuzu yeniden yapılandırmanız gerekir.
Eşzamansız doğası nedeniyle, eşzamansız return
bir akıştan geri aramanın tanımlandığı eşzamanlı akışa geri dönmenin imkansız olduğunu da fark edeceksiniz , çünkü eşzamansız geri aramalar, eşzamanlı kodun yürütülmesi bittikten uzun süre sonra yürütülür.
return
Eşzamansız bir geri aramadan bir değer almak yerine , geri arama modelini veya ... Promises'i kullanmanız gerekecektir.
Sözler
Tutmak için yollar bulunmasına rağmen geri arama cehennem vanilya JS ile koyunda, vaat popülaritesi artmaktadır ve halen ES6 içinde standardize ediliyor (bkz - MDN Promise ).
Promises (aka Futures), eşzamansız kodun daha doğrusal ve dolayısıyla hoş bir şekilde okunmasını sağlar, ancak tüm işlevlerini açıklamak bu sorunun kapsamı dışındadır. Bunun yerine, bu mükemmel kaynakları ilgilenenlere bırakacağım:
JavaScript eşzamansızlığı hakkında daha fazla okuma materyali
- Düğüm Sanatı - Geri Çağırma , asenkron kodu ve geri çağrıları vanilya JS örnekleri ve Node.js kodu ile çok iyi açıklar.
Not: Bu yanıtı Topluluk Wiki olarak işaretledim, bu nedenle en az 100 itibara sahip herkes onu düzenleyebilir ve geliştirebilir! Lütfen bu yanıtı geliştirmekten çekinmeyin veya isterseniz tamamen yeni bir yanıt gönderin.
Ajax ile ilgisi olmayan eşzamansızlık sorunlarına yanıt vermek için bu soruyu kanonik bir konuya dönüştürmek istiyorum (bunun için bir AJAX çağrısından yanıt nasıl geri getirilir? ), Bu nedenle bu konunun olabildiğince iyi ve yararlı olması için yardımınıza ihtiyacı var !
Fabrício'nun yanıtı yerinde; ama onun cevabını, eşzamansızlık kavramını açıklamaya yardımcı olacak bir analojiye odaklanan daha az teknik bir şeyle tamamlamak istedim .
Bir Analoji ...
Dün, yaptığım iş bir meslektaşımdan bazı bilgiler gerektirdi. Onu aradım; konuşma şöyle geçti:
Beni : Merhaba Bob, nasıl bilmek gerekir foo 'd çubuğu ' geçen hafta d. Jim bununla ilgili bir rapor istiyor ve bunun ayrıntılarını bilen tek kişi sensin.
Bob : Elbette, ama 30 dakika kadar sürer mi?
Ben : Bu harika Bob. Bilgiye sahip olunca beni ara!
Bu noktada telefonu kapattım. Raporumu tamamlamak için Bob'dan bilgi almam gerektiğinden, raporu bıraktım ve onun yerine bir kahve içmeye gittim, sonra bir e-postayı yakaladım. 40 dakika sonra (Bob yavaş), Bob aradı ve bana ihtiyacım olan bilgiyi verdi. Bu noktada, ihtiyacım olan tüm bilgilere sahip olduğum için raporumla işime devam ettim.
Bunun yerine sohbetin böyle gittiğini hayal edin;
Beni : Merhaba Bob, nasıl bilmek gerekir foo 'd çubuğu ' geçen hafta d. Jim, bununla ilgili bir rapor ve bununla ilgili ayrıntıları bilen tek kişi sensin.
Bob : Elbette, ama 30 dakika kadar sürer mi?
Ben : Bu harika Bob. Bekleyeceğim.
Orada oturdum ve bekledim. Ve bekledi. Ve bekledi. 40 dakikadır. Beklemekten başka bir şey yapmamak. Sonunda Bob bana bilgi verdi, kapattık ve raporumu tamamladım. Ama 40 dakikalık üretkenliği kaybettim.
Bu eşzamansız ve eşzamanlı davranıştır
Sorumuzdaki tüm örneklerde olan tam olarak budur. Bir görüntüyü yüklemek, diskten bir dosya yüklemek ve AJAX aracılığıyla bir sayfa talep etmek, hepsi yavaş işlemlerdir (modern bilgi işlem bağlamında).
JavaScript, bu yavaş işlemlerin tamamlanmasını beklemek yerine, yavaş işlem tamamlandığında çalıştırılacak bir geri arama işlevi kaydetmenize izin verir. Ancak bu arada, JavaScript diğer kodları çalıştırmaya devam edecektir. JavaScript'in yavaş işlemin tamamlanmasını beklerken başka bir kod çalıştırması , davranışı eşzamansız hale getirir . JavaScript başka bir kodu çalıştırmadan önce işlemin tamamlanmasını bekleseydi, bu eşzamanlı bir davranış olurdu .
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);
Yukarıdaki kodda lolcat.png
, bir sloooow işlemi olan JavaScript'in yüklenmesini istiyoruz . Geri çağırma işlevi, bu yavaş işlem tamamlandıktan sonra çalıştırılacaktır, ancak bu arada JavaScript sonraki kod satırlarını işlemeye devam edecektir; yani alert(outerScopeVar)
.
Bu nedenle uyarıların gösterildiğini görüyoruz undefined
; çünkü alert()
hemen işlenir ziyade görüntü yüklendikten sonra.
Bizim kodunu çözmek için, yapmamız gereken tüm taşımaktır alert(outerScopeVar)
kod içine geri arama işlevi. Bunun bir sonucu olarak, artık outerScopeVar
global değişken olarak bildirilen değişkene ihtiyacımız yok .
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Her zaman bir geri aramanın işlev olarak belirtildiğini görürsünüz, çünkü JavaScript'te bazı kodu tanımlamanın tek * yolu budur, ancak daha sonraya kadar çalıştırmaz.
Bu nedenle, tüm örneklerimizde function() { /* Do something */ }
geri çağırma; tüm örnekleri düzeltmek için tek yapmamız gereken işlemin yanıtına ihtiyaç duyan kodu oraya taşımak!
* Teknik eval()
olarak da kullanabilirsiniz , ancak bu amaç için eval()
kötüdür
Arayan kişiyi nasıl bekletebilirim?
Şu anda buna benzer bir kodunuz olabilir;
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);
Ancak artık bunun return outerScopeVar
hemen gerçekleştiğini biliyoruz ; onload
geri çağırma işlevi değişkeni güncellemeden önce . Bu, getWidthOfImage()
geri dönmeye undefined
ve undefined
uyarılmaya yol açar .
Bunu düzeltmek için, çağıran işlevin getWidthOfImage()
bir geri aramayı kaydetmesine izin vermeli, ardından genişliğin uyarısını bu geri arama içinde olacak şekilde hareket ettirmeliyiz;
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);
});
... daha önce olduğu gibi, global değişkenleri kaldırabildiğimize dikkat edin (bu durumda width
).
İşte hızlı bir referans arayan insanlar için daha kısa bir cevap ve vaatler ve eşzamansız / bekleme kullanan bazı örnekler.
Eşzamansız bir yöntemi çağıran (bu durumda setTimeout
) ve bir mesaj döndüren bir işlev için (işe yaramayan) saf yaklaşımla başlayın :
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
bu durumda günlüğe kaydedilir çünkü getMessage
geri setTimeout
arama çağrılmadan ve güncellenmeden önce döner outerScopeVar
.
Bunu çözmenin iki ana yolu geri aramaları ve vaatleri kullanmaktır :
Geri aramalar
Buradaki değişiklik , sonuçları mevcut olduğunda çağıran koda geri göndermek için çağrılacak getMessage
bir callback
parametreyi kabul etmesidir .
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Sözler, geri aramalardan daha esnek bir alternatif sağlar çünkü bunlar, birden çok eşzamansız işlemi koordine etmek için doğal olarak birleştirilebilir. Bir Promises / A + standart uygulaması, node.js (0.12+) ve birçok mevcut tarayıcıda yerel olarak sağlanır, ancak Bluebird ve Q gibi kitaplıklarda da uygulanır .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Ertelenenler
jQuery, Ertelenenleri ile vaatlere benzer işlevsellik sağlar.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
eşzamansız / bekleyin
JavaScript ortamınız async
ve await
(Node.js 7.6+ gibi) için destek içeriyorsa , vaatleri async
işlevler içinde eşzamanlı olarak kullanabilirsiniz :
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();
Bariz olanı ifade etmek için kupa temsil eder outerScopeVar
.
Eşzamansız işlevler şöyle olabilir ...
Diğer cevaplar mükemmel ve ben sadece buna basit bir cevap vermek istiyorum. Sadece jQuery eşzamansız çağrılarla sınırlama
Tüm ajax çağrıları ( $.get
veya $.post
veya dahil $.ajax
) asenkrondur.
Örneğinizi düşününce
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
Kod yürütme 1. satırdan başlar, 2. satırdaki değişkeni ve tetikleyicileri ve eşzamansız çağrıyı bildirir (yani talep sonrası) ve son talebin yürütülmesini tamamlamasını beklemeden 3. satırdan yürütmeye devam eder.
Gönderi isteğinin tamamlanmasının 10 saniye sürdüğünü söyleyelim, değeri outerScopeVar
ancak bu 10 saniye sonra ayarlanacaktır.
Denemek için,
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
Şimdi bunu çalıştırdığınızda, 3. satırda bir uyarı alırsınız. Şimdi, gönderi isteğinin bir değer döndürdüğünden emin olana kadar bir süre bekleyin. Ardından, uyarı kutusunda Tamam'a tıkladığınızda, bir sonraki uyarı, beklediğiniz için beklenen değeri yazdırır.
Gerçek hayat senaryosunda kod,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Eşzamansız çağrılara bağlı olan tüm kod, eşzamansız bloğun içine veya eşzamansız çağrıları bekleyerek taşınır.
Tüm bu senaryolarda outerScopeVar
, eşzamansız olarak değiştirilir veya bir değer atanır veya daha sonraki bir zamanda meydana gelir (bazı olayların meydana gelmesi için bekleme veya dinleme), mevcut yürütme beklemeyecektir. Yani tüm bu durumlarda, mevcut yürütme akışıouterScopeVar = undefined
Her bir örneği tartışalım (eşzamansız olarak adlandırılan veya bazı olayların meydana gelmesi için geciken kısmı işaretledim):
1.
Burada o belirli olay üzerine yürütülecek bir olay listesi kaydederiz. Burada görüntünün yüklenmesi.Sonra mevcut yürütme sonraki satırlarla devam eder img.src = 'lolcat.png';
ve alert(outerScopeVar);
bu arada olay gerçekleşmeyebilir. yani işlev img.onload
, belirtilen görüntünün eşzamanlı olarak yüklenmesini bekler. Bu, aşağıdaki tüm örnekte gerçekleşecektir - olay farklı olabilir.
2.
Burada zaman aşımı olayı, belirtilen süreden sonra işleyiciyi çağıracak rolü oynar. İşte burada 0
, ancak yine de eşzamansız bir olayı kaydeder Event Queue
, bu, garantili gecikmeyi yapan için yürütmenin son konumuna eklenecektir .
3.
4.
Düğüm, eşzamansız kodlamanın kralı olarak düşünülebilir.Burada işaretli işlev, belirtilen dosyayı okuduktan sonra yürütülecek bir geri arama işleyicisi olarak kaydedilir.
5.
Açık sözler (gelecekte bir şeyler yapılacak) eşzamansızdır. bkz JavaScript Ertelenmiş, Promise ve Geleceğe arasındaki farklar nelerdir?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript