Dlaczego moja zmienna pozostaje niezmieniona po zmodyfikowaniu jej wewnątrz funkcji? - Asynchroniczny kod odniesienia

Biorąc pod uwagę następujące przykłady, dlaczego outerScopeVar jest niezdefiniowany we wszystkich przypadkach?

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 wyświetla undefined we wszystkich tych przykładach? Nie chcę obejść, chcę wiedzieć, dlaczego to się dzieje.


Uwaga: jest to kanoniczne pytanie dla Asynchroniczności JavaScript . Zachęcamy do ulepszenia tego pytania i dodania bardziej uproszczonego przykłady, z którymi Wspólnota może się utożsamiać.

Author: Michał Perłakowski, 2014-05-15

6 answers

Odpowiedź jednym słowem: asynchroniczność .

Forewords

Ten temat był powtarzany co najmniej kilka tysięcy razy, tutaj, w Stack Overflow. W związku z tym, po pierwsze chciałbym zwrócić uwagę na kilka niezwykle przydatnych zasobów: {]}


Odpowiedź na pytanie pod ręką

Najpierw prześledzimy wspólne zachowanie. We wszystkich przykładach outerScopeVar jest modyfikowana wewnątrz funkcji. Ta funkcja nie jest wykonywana natychmiast, jest przypisywana lub przekazane jako argument. To właśnie nazywamy callback.

Teraz pytanie brzmi, kiedy to wywołanie callback?

To zależy od sprawy. Spróbujmy jeszcze raz prześledzić jakieś typowe zachowanie: {]}
  • {[3] } może być wywołane w przyszłości , kiedy (i jeśli) obraz zostanie pomyślnie załadowany.
  • setTimeout może być wywołanakiedyś w przyszłości , po tym, jak opóźnienie upłynęło, a limit czasu nie został anulowany przez clearTimeout. Uwaga: nawet jeśli używasz 0 jako opóźnienia, wszystkie przeglądarki mają minimalny limit opóźnienia (określony jako 4MS w specyfikacji HTML5).
  • Wywołanie zwrotne jQuery $.post może być wywołane kiedyś w przyszłości, kiedy (i jeśli) żądanie Ajax zostało pomyślnie zakończone.
  • węzeł.js fs.readFile może być wywołany kiedyś w przyszłości , gdy plik został odczytany pomyślnie lub wystąpił błąd.

We wszystkich przypadkach, mamy callback, który może uruchomić kiedyś w przyszłość . To" kiedyś w przyszłości " jest tym, co nazywamy asynchronicznym przepływem .

Wykonanie asynchroniczne jest wypychane z przepływu synchronicznego. Oznacza to, że kod asynchroniczny nigdy nie będzie wykonywany podczas wykonywania stosu kodu synchronicznego. Takie jest znaczenie języka JavaScript jako jednowątkowego.

Dokładniej, gdy silnik JS jest bezczynny-nie wykonuje stosu (a) kodu synchronicznego - będzie sprawdzał zdarzenia, które mogą mieć wyzwalane asynchroniczne wywołania zwrotne (np. wygasły timeout, odebrana odpowiedź sieciowa) i wykonują je jeden po drugim. Jest to traktowane jako pętla zdarzeń .

Kod asynchroniczny podświetlony na czerwono może zostać wykonany dopiero po wykonaniu wszystkich pozostałych kodów synchronicznych w odpowiednich blokach kodu:]}

podświetlony kod asynchroniczny

Krótko mówiąc, funkcje wywołania zwrotnego są tworzone synchronicznie, ale wykonywane asynchronicznie. Po prostu nie możesz polegać na wykonywanie funkcji asynchronicznej, dopóki nie wiesz, że została wykonana, i jak to zrobić?

To naprawdę proste. Logika zależna od wykonania funkcji asynchronicznej powinna być uruchamiana / wywoływana z wewnątrz tej funkcji asynchronicznej. Na przykład, przeniesienie alertS I console.log S zbyt wewnątrz funkcji wywołania zwrotnego spowoduje oczekiwany wynik, ponieważ wynik jest dostępny w tym momencie.

Implementowanie własnej logiki wywołania zwrotnego

Często trzeba zrobić więcej rzeczy z wynikiem z funkcji asynchronicznej lub zrobić różne rzeczy z wynikiem w zależności od tego, gdzie funkcja asynchroniczna została wywołana. Zajmijmy się nieco bardziej złożonym przykładem:

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

Uwaga: używam setTimeout z losowym opóźnieniem jako ogólnej funkcji asynchronicznej, ten sam przykład dotyczy Ajax, readFile, onload i każdy inny przepływ asynchroniczny.

Ten przykład wyraźnie cierpi na ten sam problem, co inne przykłady, nie czeka dopóki funkcja asynchroniczna nie zostanie wykonana.

Zajmijmy się tym wdrażając własny system oddzwaniania. Po pierwsze, pozbędziemy się tego brzydkiego outerScopeVar, który jest całkowicie bezużyteczny w tym przypadku. Następnie dodajemy parametr, który akceptuje argument funkcji, nasz callback. Po zakończeniu operacji asynchronicznej wywołujemy to wywołanie zwrotne przekazujące wynik. Realizacja (proszę przeczytać komentarze 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);
}

Najczęściej w rzeczywistych przypadkach użycia, DOM API i większość bibliotek już podaj funkcjonalność wywołania zwrotnego (implementacja helloCatAsync w tym demonstracyjnym przykładzie). Wystarczy tylko przekazać funkcję zwrotną i zrozumieć, że będzie ona wykonywać z przepływu synchronicznego i zrestrukturyzować kod, aby pomieścić do tego.

Można również zauważyć, że ze względu na asynchroniczny charakter, niemożliwe jest return wartość z asynchronicznego przepływu z powrotem do synchronicznego przepływu, 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 returnING wartość z asynchronicznego wywołania zwrotnego, będziesz musiał użyć wzorca wywołania zwrotnego, or. .. Obietnice.

Obietnice

Chociaż istnieją sposoby, aby utrzymać callback hell na dystans z vanilla JS, promises cieszą się coraz większą popularnością i są obecnie standaryzowane w ES6 (zobacz Promise - MDN ).

Obietnice (a.k.a. Futures) zapewniają bardziej liniowy, a co za tym idzie przyjemna lektura kodu asynchronicznego, ale wyjaśnienie całej ich funkcjonalności nie wchodzi w zakres tego pytania. Zamiast tego zostawię te doskonałe zasoby dla zainteresowanych: {]}


Więcej materiałów na temat Asynchroniczności JavaScript


Uwaga: zaznaczyłem tę odpowiedź jako Community Wiki, stąd każdy, kto ma przynajmniej 100 reputacji, może ją edytować i ulepszać! Prosimy o poprawienie tej odpowiedzi lub przesłanie zupełnie nowej odpowiedzi, jeśli chcesz, jak również.

Chcę przekształcić to pytanie w kanoniczny temat, aby odpowiedzieć na problemy asynchroniczności, które nie są związane z Ajaxem (jest Jak zwrócić odpowiedź z połączenia AJAX? do tego), dlatego ten temat potrzebuje twojej pomocy, aby być tak dobrym i pomocnym, jak to możliwe!

 460
Author: Fabrício Matté,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-12 13:14:27

odpowiedź Fabrício jest trafna, ale chciałem uzupełnić jego odpowiedź o coś mniej technicznego, co skupia się na analogii, aby pomóc wyjaśnić pojęcie asynchroniczności [22].


Analogia...

Wczoraj moja praca wymagała pewnych informacji od kolegi. Zadzwoniłem do niego; Oto jak przebiegła rozmowa:

Ja : Cześć Bob, muszę wiedzieć, jak my foo 'D bar 'D w zeszłym tygodniu. Jim chce zgłoś to, a tylko Ty znasz szczegóły.

Bob: jasne, ale zajmie mi to około 30 minut?

Ja: to świetnie Bob. Oddzwoń, jak będziesz miał informacje!

W tym momencie rozłączyłem się. Ponieważ potrzebowałem informacji od Boba, aby zakończyć mój raport, zostawiłem raport i poszedłem na kawę, a następnie nadrobiłem trochę e-maila. 40 minut później (Bob jest powolny), Bob oddzwonił i dał mi informacje potrzebne. W tym momencie wznowiłem pracę z moim sprawozdaniem, ponieważ miałem wszystkie potrzebne informacje.

Wyobraź sobie, że rozmowa potoczyłaby się w ten sposób;

Ja : Cześć Bob, muszę wiedzieć, jak my foo 'D bar 'D w zeszłym tygodniu. Jim chce raportu i tylko Ty znasz szczegóły.

Bob: jasne, ale zajmie mi to jakieś 30 minut?

Ja: to świetnie Bob. Poczekam.

Siedziałam tam i czekałam. I czekał. I czekał. Przez 40 minut. Nie robiąc nic poza czekaniem. W końcu Bob dał mi informacje, rozłączyliśmy się i skończyłem raport. Ale straciłem 40 minut produktywności.
Jest to zachowanie asynchroniczne i synchroniczne.]}

To jest dokładnie to, co dzieje się we wszystkich przykładach w naszym pytaniu. Wczytywanie obrazu, wczytywanie pliku z dysku i żądanie strony za pomocą AJAX to powolne operacje (w kontekście nowoczesnych komputerów).

Zamiast oczekiwania na zakończenie tych powolnych operacji, JavaScript pozwala zarejestrować funkcję zwrotną, 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 czekając na powolne operacja zakończona powoduje, że zachowaniejest asynchroniczne. Gdyby JavaScript czekał na zakończenie operacji przed wykonaniem innego kodu, byłoby to zachowanie synchronous .

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 JavaScript o załadowanie lolcat.png, co jest operacją sloooow. Funkcja callback zostanie wykonana po wykonaniu tej powolnej operacji, ale w międzyczasie JavaScript będzie nadal przetwarzał kolejne wiersze kodu; tj. alert(outerScopeVar).

Dlatego widzimy alert pokazujący undefined; ponieważ {[7] } jest przetwarzany natychmiast, a nie po załadowaniu obrazu.

Aby naprawić nasz kod, wystarczy przenieść alert(outerScopeVar) kod do funkcji callback. W konsekwencji nie potrzebujemy już zmiennej outerScopeVar zadeklarowanej jako zmienna globalna.

var img = document.createElement('img');

img.onload = function() {
    var localScopeVar = this.width;
    alert(localScopeVar);
};

img.src = 'lolcat.png';

Będziesz zawsze zobacz callback jest określony jako funkcja, ponieważ jest to jedyny * sposób w JavaScript, aby zdefiniować jakiś kod, ale nie wykonać go dopiero później.

Dlatego we wszystkich naszych przykładach, function() { /* Do something */ } jest wywołaniem zwrotnym; aby naprawić wszystkie przykłady, wszystko, co musimy zrobić, to przenieść kod, który wymaga reakcji operacji tam!

* technicznie można również użyć eval(), ale eval() jest złem w tym celu


Jak sprawić, by mój rozmówca czekał?

Możesz mieć obecnie kod podobny do to;

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ę natychmiast; przed onload funkcja callback zaktualizowała zmienną. To prowadzi do getWidthOfImage() powrotu undefined i undefined bycia zaalarmowanym.

Aby to naprawić, musimy zezwolić funkcji wywołującej getWidthOfImage(), aby zarejestrować wywołanie zwrotne, a następnie przesunąć alert ' ING o szerokości, która będzie w tym wywołaniu zwrotnym;
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);
});

... tak jak wcześniej, zauważ, że udało nam się usunąć zmienne globalne (w tym przypadku width).

 128
Author: Matt,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2018-06-12 13:16:20

Oto bardziej zwięzła odpowiedź dla osób, które szukają szybkiego odniesienia, a także kilka przykładów z wykorzystaniem obietnic i asynchronicznych / oczekujących.

Zacznij od naiwnego podejścia (które nie działa) dla funkcji, która wywołuje metodę asynchroniczną (w tym przypadku setTimeout) i zwraca wiadomość:

function getMessage() {
  var outerScopeVar;
  setTimeout(function() {
    outerScopeVar = 'Hello asynchronous world!';
  }, 0);
  return outerScopeVar;
}
console.log(getMessage());

undefined jest zalogowany w tym przypadku, ponieważ getMessage zwraca przed wywołaniem wywołania zwrotnego setTimeout i aktualizacją outerScopeVar.

Dwa główne sposoby jej rozwiązania to użycie callbacks and promises :

Callbacks

Zmiana polega na tym, że getMessage przyjmuje parametr callback, 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

Obietnice zapewniają alternatywę, która jest bardziej elastyczna niż wywołania zwrotne, ponieważ mogą być naturalnie łączone w celu koordynowania wielu operacji asynchronicznych. A obietnice/a+ standardowa implementacja jest natywnie dostarczany w node.js (0.12+) i wielu aktualnych przeglądarkach, ale jest również zaimplementowany w bibliotekach takich 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

JQuery zapewnia funkcjonalność, która jest podobna do obietnic z jego Deferreds.

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

Async / wait

Jeśli Twoje środowisko JavaScript zawiera wsparcie dla async oraz await (Jak Node.js 7.6+), wtedy można użyć obietnic synchronously within async functions:

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();
 57
Author: JohnnyHK,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-11-22 05:45:24

Aby stwierdzić oczywistość, Puchar reprezentuje outerScopeVar.

Funkcje asynchroniczne są podobne...

asynchroniczne wezwanie do kawy

 48
Author: Johannes Fahrenkrug,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2015-12-08 17:01:14

Inne odpowiedzi są doskonałe i chcę tylko udzielić prostej odpowiedzi na to pytanie. Po prostu ograniczam się do asynchronicznych wywołań 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ą oraz wyzwalacze i asynchroniczne wywołanie w linii 2 (tzn. żądanie post) i kontynuuje jego wykonywanie z linii 3, nie czekając na żądanie post do dokończ jego wykonanie.

Powiedzmy, że zakończenie żądania post zajmuje 10 sekund, wartość outerScopeVar zostanie ustawiona dopiero po tych 10 sekundach.

Aby wypró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
Kiedy to wykonasz, otrzymasz alarm na linii 3. Teraz poczekaj jakiś czas, aż będziesz pewien, że żądanie post zwróciło jakąś wartość. Następnie po kliknięciu OK, w polu alert, następny alert wydrukuje wartość oczekiwaną, ponieważ czekałeś na nią.

W realnym scenariuszu, kod staje się,

var outerScopeVar;
$.post('loldog', function(response) {
    outerScopeVar = response;
    alert(outerScopeVar);
});

Cały kod zależny od wywołań asynchronicznych jest przenoszony wewnątrz bloku asynchronicznego lub przez oczekiwanie na wywołania asynchroniczne.

 11
Author: Teja,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2016-02-26 03:59:09

We wszystkich tych scenariuszach outerScopeVar jest modyfikowana lub przypisywana wartość asynchronicznie lub dzieje się w późniejszym czasie (oczekiwanie lub nasłuchiwanie jakiegoś zdarzenia),na które bieżące wykonanie nie będzie czekać .]}

Omówmy każdy przykład (zaznaczyłem część, która jest nazywana asynchronicznie lub opóźniona dla niektórych zdarzeń):

1.

Tutaj wpisz opis obrazka

Tutaj rejestrujemy eventlistner, który zostanie wykonany na danym wydarzeniu.Tutaj ładowanie obrazu.Następnie bieżące wykonanie ciągłe z kolejnymi liniami img.src = 'lolcat.png'; i alert(outerScopeVar); tymczasem zdarzenie może nie wystąpić. tj. funtion img.onload Poczekaj, aż dany obraz załaduje się asynchronicznie. Stanie się tak cały następujący przykład - zdarzenie może się różnić.

2.

2

Tutaj rolę odgrywa Zdarzenie timeout, które wywoła procedurę obsługi po określonym czasie. Proszę. jest 0, ale nadal rejestruje zdarzenie asynchroniczne, które zostanie dodane do ostatniej pozycji Event Queue do wykonania, co powoduje gwarantowane opóźnienie.

3.

Tutaj wpisz opis obrazka Tym razem Ajax callback.

4.

Tutaj wpisz opis obrazka

Węzeł może być uważany za króla kodowania asynchronicznego.Tutaj zaznaczona funkcja jest rejestrowana jako Obsługa wywołania zwrotnego, która zostanie wykonana po odczytaniu podanego plik.

5.

Tutaj wpisz opis obrazka

Oczywista obietnica (coś zostanie zrobione w przyszłości) jest asynchroniczna. zobacz jakie są różnice między odroczeniem, obietnicą i przyszłością w JavaScript?

Https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

 8
Author: Tom Sebastian,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:26:36