Dlaczego moja zmienna pozostaje niezmieniona po zmodyfikowaniu jej w funkcji? - Odwołanie do kodu asynchronicznego
Biorąc pod uwagę poniższe przykłady, dlaczego outerScopeVar
we wszystkich przypadkach jest ono nieokreślone?
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);
Dlaczego pojawia się undefined
we wszystkich tych przykładach? Nie chcę obejść, chcę wiedzieć, dlaczego tak się dzieje.
Uwaga: jest to kanoniczne pytanie dotyczące asynchroniczności JavaScript . Zapraszam do poprawienia tego pytania i dodania bardziej uproszczonych przykładów, z którymi społeczność może się identyfikować.
Odpowiedzi
Odpowiedź jednym słowem: asynchroniczność .
Przedmowa
Ten temat był powtarzany co najmniej kilka tysięcy razy tutaj, w przepełnieniu stosu. Dlatego najpierw chciałbym wskazać kilka niezwykle przydatnych zasobów:
Odpowiedź @Felixa Klinga na pytanie „Jak zwrócić odpowiedź z wywołania asynchronicznego?” . Zobacz jego doskonałą odpowiedź wyjaśniającą przepływy synchroniczne i asynchroniczne, a także sekcję „Restructure code”.
@Benjamin Gruenbaum również włożył wiele wysiłku w wyjaśnienie asynchroniczności w tym samym wątku.Odpowiedź @Matt Esch na temat „Pobierz dane z fs.readFile” również bardzo dobrze wyjaśnia asynchroniczność w prosty sposób.
Odpowiedź na zadane pytanie
Prześledźmy najpierw typowe zachowanie. We wszystkich przykładach outerScopeVar
znak jest modyfikowany wewnątrz funkcji . Ta funkcja najwyraźniej nie jest wykonywana od razu, jest przypisywana lub przekazywana jako argument. To właśnie nazywamy oddzwonieniem .
Teraz pytanie brzmi, kiedy wywoływane jest to oddzwonienie?
To zależy od przypadku. Spróbujmy ponownie prześledzić niektóre typowe zachowania:
img.onload
może zostać wywołany kiedyś w przyszłości , kiedy (i jeśli) obraz zostanie pomyślnie załadowany.setTimeout
może zostać wywołany w przyszłości , po upływie opóźnienia, a limit czasu nie został anulowany przezclearTimeout
. Uwaga: nawet w przypadku używania0
jako opóźnienia wszystkie przeglądarki mają minimalny limit czasu opóźnienia (określony jako 4 ms w specyfikacji HTML5).- Funkcja
$.post
zwrotna jQuery może zostać wywołana kiedyś w przyszłości , kiedy (i jeśli) żądanie Ajax zostało pomyślnie zakończone. - Node.js
fs.readFile
może zostać wywołany kiedyś w przyszłości , gdy plik zostanie pomyślnie odczytany lub zgłosi błąd.
We wszystkich przypadkach mamy wywołanie zwrotne, które może zostać uruchomione kiedyś w przyszłości . To „kiedyś w przyszłości” nazywamy przepływem asynchronicznym .
Wykonywanie asynchroniczne jest wypychane z przepływu synchronicznego. Oznacza to, że kod asynchroniczny nigdy nie zostanie wykonany podczas wykonywania synchronicznego stosu kodu. To jest znaczenie jednowątkowego JavaScript.
Dokładniej, gdy silnik JS jest bezczynny - nie wykonuje stosu (a) kodu synchronicznego - będzie odpytywał pod kątem zdarzeń, które mogły wyzwolić asynchroniczne wywołania zwrotne (np. Przekroczony limit czasu, odebrana odpowiedź sieciowa) i wykonywał je jeden po drugim. Nazywa się to pętlą zdarzeń .
Oznacza to, że kod asynchroniczny podświetlony ręcznie narysowanymi czerwonymi kształtami może zostać wykonany dopiero po wykonaniu całego pozostałego kodu synchronicznego w odpowiednich blokach kodu:
Krótko mówiąc, funkcje zwrotne są tworzone synchronicznie, ale wykonywane asynchronicznie. Po prostu nie możesz polegać na wykonaniu funkcji asynchronicznej, dopóki nie wiesz, że została wykonana, i jak to zrobić?
To naprawdę proste. Logika zależna od wykonywania funkcji asynchronicznej powinna być uruchamiana / wywoływana z wnętrza tej funkcji asynchronicznej. Na przykład przeniesienie alert
s i console.log
s również do funkcji wywołania zwrotnego dałoby oczekiwany wynik, ponieważ wynik jest dostępny w tym momencie.
Implementacja własnej logiki wywołań zwrotnych
Często trzeba zrobić więcej rzeczy z wynikiem funkcji asynchronicznej lub zrobić różne rzeczy z wynikiem w zależności od miejsca wywołania funkcji asynchronicznej. Spójrzmy na nieco bardziej złożony przykład:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Uwaga: Używam setTimeout
z losowym opóźnieniem jako rodzajowy funkcji asynchronicznego, ten sam przykład odnosi się do Ajaksu readFile
, onload
a także wszelkie inne przepływu asynchroniczny.
Ten przykład wyraźnie cierpi z powodu tego samego problemu, co inne przykłady, nie czeka na wykonanie funkcji asynchronicznej.
Zajmijmy się tym zaimplementowanie własnego systemu wywołań zwrotnych. Po pierwsze, pozbywamy się tego brzydkiego, outerScopeVar
który w tym przypadku jest całkowicie bezużyteczny. Następnie dodajemy parametr, który przyjmuje argument funkcji, nasz callback. Po zakończeniu operacji asynchronicznej wywołujemy to wywołanie zwrotne, przekazując wynik. Realizacja (prosimy o zapoznanie się z komentarzami w kolejności):
// 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);
}
Fragment kodu z powyższego przykładu:
// 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);
}
Najczęściej w rzeczywistych przypadkach DOM API i większość bibliotek już zapewnia funkcjonalność wywołania zwrotnego ( helloCatAsync
implementacja w tym przykładowym przykładzie). Musisz tylko przekazać funkcję zwrotną i zrozumieć, że będzie ona wykonywana poza przepływem synchronicznym, i zrestrukturyzować kod, aby to dostosować.
Zauważysz również, że ze względu na asynchroniczny charakter nie jest możliwa return
wartość z przepływu asynchronicznego z powrotem do przepływu synchronicznego, w którym zostało zdefiniowane wywołanie zwrotne, ponieważ asynchroniczne wywołania zwrotne są wykonywane długo po zakończeniu wykonywania kodu synchronicznego.
Zamiast return
pobierać wartość z asynchronicznego wywołania zwrotnego, będziesz musiał skorzystać z wzorca wywołania zwrotnego lub ... Promises.
Obietnice
Chociaż istnieją sposoby na powstrzymanie piekła zwrotnego za pomocą waniliowego JS, obietnice zyskują na popularności i są obecnie standaryzowane w ES6 (patrz Promise - MDN ).
Obietnice (aka Futures) zapewniają bardziej liniowy, a przez to przyjemny odczyt kodu asynchronicznego, ale wyjaśnienie całej ich funkcjonalności jest poza zakresem tego pytania. Zamiast tego zostawię te doskonałe zasoby zainteresowanym:
Więcej materiałów do czytania na temat asynchroniczności JavaScript
- Sztuka węzła - wywołania zwrotne bardzo dobrze objaśniają kod asynchroniczny i wywołania zwrotne za pomocą prostych przykładów JS i kodu Node.js.
Uwaga: oznaczyłem tę odpowiedź jako Wiki społeczności, dlatego każdy, kto ma co najmniej 100 reputacji, może ją edytować i ulepszać! Prosimy o ulepszenie tej odpowiedzi lub przesłanie zupełnie nowej odpowiedzi, jeśli chcesz.
Chcę przekształcić to pytanie w kanoniczny temat, aby odpowiedzieć na problemy związane z asynchronicznością, które nie są związane z Ajaxem (jest Jak zwrócić odpowiedź z połączenia AJAX? W tym celu), dlatego ten temat potrzebuje Twojej pomocy, aby był jak najlepszy i pomocny !
Odpowiedź Fabrício jest trafna; ale chciałem uzupełnić jego odpowiedź czymś mniej technicznym, skupiającym się na analogii, która pomoże wyjaśnić pojęcie asynchroniczności .
Analogia ...
Wczoraj wykonywana przeze mnie praca wymagała informacji od kolegi. Zadzwoniłem do niego; oto jak przebiegła rozmowa:
Ja : Cześć Bob, muszę wiedzieć, jak radziliśmy sobie w barze w zeszłym tygodniu. Jim chce raportu na ten temat, a ty jesteś jedyną osobą, która zna szczegóły.
Bob : Jasne, ale zajmie mi to około 30 minut?
Ja : To świetnie Bob. Oddzwoń, kiedy zdobędziesz informacje!
W tym momencie odłożyłem słuchawkę. Ponieważ potrzebowałem informacji od Boba, aby ukończyć raport, opuściłem go i poszedłem na kawę, a potem złapałem kilka e-maili. 40 minut później (Bob jest wolny), Bob oddzwonił i przekazał mi potrzebne informacje. W tym momencie wznowiłem pracę z raportem, ponieważ posiadałem wszystkie potrzebne informacje.
Wyobraź sobie, że zamiast tego rozmowa potoczyłaby się w ten sposób;
Ja : Cześć Bob, muszę wiedzieć, jak radziliśmy sobie w barze w zeszłym tygodniu. Jim chce raportu na ten temat, a ty jesteś jedyną osobą, która zna szczegóły.
Bob : Jasne, ale zajmie mi to około 30 minut?
Ja : To świetnie Bob. Poczekam.
Siedziałem tam i czekałem. I czekałem. I czekałem. Przez 40 minut. Nie robię nic poza czekaniem. W końcu Bob przekazał mi informacje, odłożyliśmy słuchawkę i dokończyłem raport. Ale straciłem 40 minut produktywności.
Jest to zachowanie asynchroniczne i synchroniczne
Dokładnie to dzieje się we wszystkich przykładach w naszym pytaniu. Ładowanie obrazu, ładowanie pliku z dysku i żądanie strony przez AJAX to powolne operacje (w kontekście współczesnych komputerów).
Zamiast czekać na zakończenie tych powolnych operacji, JavaScript umożliwia zarejestrowanie funkcji zwrotnej, która zostanie wykonana po zakończeniu powolnej operacji. W międzyczasie jednak JavaScript będzie nadal wykonywał inny kod. Fakt, że JavaScript wykonuje inny kod podczas oczekiwania na zakończenie powolnej operacji, sprawia, że zachowanie jest asynchroniczne . Gdyby JavaScript czekał na zakończenie operacji przed wykonaniem jakiegokolwiek innego kodu, byłoby to zachowanie synchroniczne .
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);
W powyższym kodzie prosimy o załadowanie JavaScript lolcat.png
, co jest powolną operacją. Funkcja zwrotna zostanie wykonana po wykonaniu tej powolnej operacji, ale w międzyczasie JavaScript będzie przetwarzał kolejne wiersze kodu; tj alert(outerScopeVar)
.
Dlatego widzimy, że alert jest widoczny undefined
; ponieważ alert()
plik jest przetwarzany natychmiast, a nie po załadowaniu obrazu.
Aby naprawić nasz kod, wszystko, co musimy zrobić, to przenieść alert(outerScopeVar)
kod do funkcji zwrotnej. W rezultacie nie potrzebujemy już outerScopeVar
zmiennej deklarowanej jako zmienna globalna.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Będziesz zawsze zobaczyć zwrotna jest określona jako funkcja, ponieważ jest to jedyny sposób w JavaScript * zdefiniować jakiś kod, ale nie wykonać ją później.
Dlatego we wszystkich naszych przykładach function() { /* Do something */ }
jest to callback; aby naprawić wszystkie przykłady, wszystko, co musimy zrobić, to przenieść tam kod, który wymaga odpowiedzi operacji!
* Technicznie możesz też użyć eval()
, ale eval()
jest zły do tego celu
Jak sprawić, by mój rozmówca czekał?
Obecnie możesz mieć kod podobny do tego;
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);
Jednak teraz wiemy, że return outerScopeVar
dzieje się to natychmiast; zanim onload
funkcja wywołania zwrotnego zaktualizuje zmienną. Prowadzi to do getWidthOfImage()
powrotu undefined
i undefined
zaalarmowania.
Aby to naprawić, musimy pozwolić funkcji wywołującej getWidthOfImage()
zarejestrować wywołanie zwrotne, a następnie przesunąć alert'ing o szerokości tak, aby znajdował się w obrębie tego wywołania zwrotnego;
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);
});
... jak poprzednio, zauważ, że byliśmy w stanie usunąć zmienne globalne (w tym przypadku width
).
Oto bardziej zwięzła odpowiedź dla osób, które szukają szybkiego odniesienia, a także kilka przykładów wykorzystujących obietnice i async / await.
Zacznij od naiwnego podejścia (które nie działa) dla funkcji, która wywołuje metodę asynchroniczną (w tym przypadku setTimeout
) i zwraca komunikat:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
zostanie zarejestrowany w tym przypadku, ponieważ getMessage
wraca przed setTimeout
wywołaniem funkcji zwrotnej i aktualizacją outerScopeVar
.
Dwa główne sposoby rozwiązania tego problemu to użycie wywołań zwrotnych i obietnic :
Callback
Zmiana polega na tym, że getMessage
akceptuje callback
parametr, który zostanie wywołany w celu dostarczenia wyników z powrotem do kodu wywołującego, gdy będzie dostępny.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Obietnice stanowią alternatywę, która jest bardziej elastyczna niż wywołania zwrotne, ponieważ można je naturalnie łączyć w celu koordynowania wielu operacji asynchronicznych. Promises / A + standardowy realizacja jest warunkiem natywnie w node.js (0.12+) oraz wielu obecnych przeglądarek, ale jest również realizowany w bibliotekach, jak Bluebird i Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Deferreds
jQuery zapewnia funkcjonalność podobną do obietnic z jej odroczeniami.
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
Jeśli Twoje środowisko JavaScript obsługuje async
i await
(jak Node.js 7.6+), możesz używać obietnic synchronicznie w ramach async
funkcji:
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();
Aby stwierdzić, co oczywiste, kielich reprezentuje outerScopeVar
.
Funkcje asynchroniczne wyglądają jak ...
Pozostałe odpowiedzi są doskonałe i chcę tylko udzielić prostej odpowiedzi na to pytanie. Ograniczam się tylko do wywołań asynchronicznych jQuery
Wszystkie wywołania AJAX (w tym $.get
lub $.post
lub $.ajax
) są asynchroniczne.
Biorąc pod uwagę twój przykład
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
Wykonywanie kodu rozpoczyna się od linii 1, deklaruje zmienną i wyzwala oraz wywołanie asynchroniczne w linii 2 (tj. Żądanie post) i kontynuuje wykonywanie od linii 3, bez czekania na zakończenie wykonywania żądania post.
Powiedzmy, że żądanie wpisu zajmuje 10 sekund, a wartość outerScopeVar
zostanie ustawiona dopiero po tych 10 sekundach.
Spróbować,
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
Teraz, kiedy to wykonasz, otrzymasz alert w linii 3. Teraz poczekaj chwilę, aż będziesz pewien, że żądanie wysłania zwróciło jakąś wartość. Następnie po kliknięciu OK w polu alertu następny alert wyświetli oczekiwaną wartość, ponieważ czekałeś na to.
W prawdziwym scenariuszu kod staje się
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Cały kod, który zależy od wywołań asynchronicznych, jest przenoszony wewnątrz bloku asynchronicznego lub przez oczekiwanie na wywołania asynchroniczne.
We wszystkich tych scenariuszach outerScopeVar
jest modyfikowana lub przypisywana wartość asynchronicznie lub dzieje się w późniejszym czasie (oczekiwanie lub nasłuchiwanie na wystąpienie jakiegoś zdarzenia), na które bieżące wykonanie nie będzie czekać .outerScopeVar = undefined
Omówmy każdy przykład (zaznaczyłem część, która jest wywoływana asynchronicznie lub opóźniona dla niektórych zdarzeń):
1.
Tutaj rejestrujemy eventlistner, który będzie wykonywany po tym konkretnym zdarzeniu, tutaj ładowanie obrazu, a następnie bieżące wykonanie jest kontynuowane z kolejnymi liniami img.src = 'lolcat.png';
i w alert(outerScopeVar);
międzyczasie zdarzenie może nie wystąpić tzn. funkcja img.onload
czeka na załadowanie wskazanego obrazu, asynchronicznie. Stanie się tak według następującego przykładu - wydarzenie może się różnić.
2.
Tutaj rolę odgrywa zdarzenie timeout, które wywoła procedurę obsługi po określonym czasie. Tutaj jest 0
, ale nadal rejestruje zdarzenie asynchroniczne, które zostanie dodane do ostatniej pozycji Event Queue
do wykonania, co zapewnia gwarantowane opóźnienie.
3.
4.
Węzeł może być traktowany jako król kodowania asynchronicznego, gdzie zaznaczona funkcja jest rejestrowana jako funkcja obsługi wywołań zwrotnych, która zostanie wykonana po odczytaniu określonego pliku.
5.
Oczywista obietnica (coś zostanie zrobione w przyszłości) jest asynchroniczna. zobacz Jakie są różnice między Deferred, Promise i Future w JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript