Dlaczego moja zmienna pozostaje niezmieniona po zmodyfikowaniu jej w funkcji? - Odwołanie do kodu asynchronicznego

May 15 2014

Biorąc pod uwagę poniższe przykłady, dlaczego outerScopeVarwe 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ę undefinedwe 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

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

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ź na zadane pytanie

Prześledźmy najpierw typowe zachowanie. We wszystkich przykładach outerScopeVarznak 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.onloadmoże zostać wywołany kiedyś w przyszłości , kiedy (i jeśli) obraz zostanie pomyślnie załadowany.
  • setTimeoutmoże zostać wywołany w przyszłości , po upływie opóźnienia, a limit czasu nie został anulowany przez clearTimeout. Uwaga: nawet w przypadku używania 0jako opóźnienia wszystkie przeglądarki mają minimalny limit czasu opóźnienia (określony jako 4 ms w specyfikacji HTML5).
  • Funkcja $.postzwrotna 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.readFilemoż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 alerts i console.logs 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 setTimeoutz losowym opóźnieniem jako rodzajowy funkcji asynchronicznego, ten sam przykład odnosi się do Ajaksu readFile, onloada 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, outerScopeVarktó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 ( helloCatAsyncimplementacja 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 returnwartość 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 returnpobierać 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 !

158 Matt May 29 2014 at 16:09

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ż outerScopeVarzmiennej 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 outerScopeVardzieje się to natychmiast; zanim onloadfunkcja wywołania zwrotnego zaktualizuje zmienną. Prowadzi to do getWidthOfImage()powrotu undefinedi undefinedzaalarmowania.

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

75 JohnnyHK Jan 21 2015 at 06:42

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

undefinedzostanie zarejestrowany w tym przypadku, ponieważ getMessagewraca przed setTimeoutwywoł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 getMessageakceptuje callbackparametr, 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 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 asynci await(jak Node.js 7.6+), możesz używać obietnic synchronicznie w ramach asyncfunkcji:

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

Aby stwierdzić, co oczywiste, kielich reprezentuje outerScopeVar.

Funkcje asynchroniczne wyglądają jak ...

14 Teja Feb 26 2016 at 10:59

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 $.getlub $.postlub $.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ść outerScopeVarzostanie 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.

11 TomSebastian Oct 27 2015 at 13:35

We wszystkich tych scenariuszach outerScopeVarjest 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.onloadczeka 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 Queuedo wykonania, co zapewnia gwarantowane opóźnienie.

3.

Tym razem wywołanie zwrotne Ajax.

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