Почему моя переменная не изменилась после того, как я изменил ее внутри функции? - асинхронная ссылка на код

670

Учитывая следующие примеры, почему не outerScopeVarопределено во всех случаях?

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

Почему он выводит undefinedво всех этих примерах? Я не хочу обходных путей, я хочу знать, почему это происходит.


Примечание. Это канонический вопрос для асинхронности JavaScript . Не стесняйтесь улучшать этот вопрос и добавлять более упрощенные примеры, с которыми сообщество может идентифицировать.

Фабрицио Матте
источник
@ Герцогское спасибо, я почти уверен, что я прокомментировал эту ссылку, но есть некоторые пропущенные комментарии. Кроме того, что касается вашего редактирования: я считаю, что наличие «канонического» и «асинхронного» в заголовке помогает при поиске этого вопроса пометить другой вопрос как обман. И, конечно, это также помогает в поиске этого вопроса от Google при поиске объяснений асинхронности.
Фабрисио Мате
3
Если немного поразмыслить, «тема канонической асинхронности» немного тяжела для названия, «ссылка на асинхронный код» проще и более объективна. Я также считаю, что большинство людей ищут «асинхронный» вместо «асинхронный».
Фабрицио Мате
1
Некоторые люди инициализируют свою переменную перед вызовом функции. Как насчет изменения названия, которое так или иначе представляет это? Например, «Почему моя переменная не изменилась после того, как я изменил ее внутри функции?» ?
Феликс Клинг
Во всех приведенных выше примерах кода "alert (outerScopeVar);" выполняется СЕЙЧАС, тогда как присвоение значения «outerScopeVar» происходит ПОЗЖЕ (асинхронно).
рефакторинг

Ответы:

543

Одним словом ответ: асинхронность .

Предисловия

Эта тема повторялась, по крайней мере, пару тысяч раз здесь, в Переполнении стека. Поэтому прежде всего я хотел бы указать на некоторые чрезвычайно полезные ресурсы:


Ответ на поставленный вопрос

Давайте сначала проследим общее поведение. Во всех примерах outerScopeVarмодификация внутри функции . Эта функция явно не выполняется сразу, она присваивается или передается в качестве аргумента. Это то, что мы называем обратным вызовом .

Теперь вопрос в том, когда вызывается этот обратный вызов?

Это зависит от случая. Давайте попробуем снова проследить какое-то общее поведение:

  • img.onloadможет быть вызвано когда-нибудь в будущем , когда (и если) изображение будет успешно загружено.
  • setTimeoutможет быть вызван когда-нибудь в будущем , после того, как задержка истекла, и тайм-аут не был отменен clearTimeout. Примечание: даже при использовании в 0качестве задержки все браузеры имеют минимальный предел времени ожидания (указанный в спецификации HTML5 равным 4 мс).
  • $.postОбратный вызов jQuery может быть вызван когда-нибудь в будущем , когда (и если) запрос Ajax будет успешно выполнен.
  • Node.js fs.readFileмогут быть вызваны когда-нибудь в будущем , когда файл будет успешно прочитан или возникла ошибка.

Во всех случаях у нас есть обратный вызов, который может быть запущен когда-нибудь в будущем . Это «когда-нибудь в будущем» мы называем асинхронным потоком .

Асинхронное выполнение выталкивается из синхронного потока. То есть асинхронный код никогда не будет выполняться во время выполнения стека синхронного кода. Это означает, что JavaScript является однопоточным.

Более конкретно, когда механизм JS находится в режиме ожидания, не выполняя стек (a) синхронного кода, он будет запрашивать события, которые могли вызвать асинхронные обратные вызовы (например, истекло время ожидания, полученный сетевой ответ), и выполнять их один за другим. Это рассматривается как Event Loop .

То есть асинхронный код, выделенный в нарисованных от руки красных формах, может выполняться только после того, как весь оставшийся синхронный код в их соответствующих кодовых блоках выполнен:

выделен асинхронный код

Короче говоря, функции обратного вызова создаются синхронно, но выполняются асинхронно. Вы просто не можете полагаться на выполнение асинхронной функции, пока не узнаете, что она выполнена, и как это сделать?

Это действительно просто. Логика, которая зависит от выполнения асинхронной функции, должна запускаться / вызываться из этой асинхронной функции. Например, перемещение alerts и console.logs внутри функции обратного вызова выдаст ожидаемый результат, потому что результат доступен в этой точке.

Реализация собственной логики обратного вызова

Часто вам нужно делать больше вещей с результатом асинхронной функции или делать разные вещи с результатом в зависимости от того, где была вызвана асинхронная функция. Давайте рассмотрим более сложный пример:

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

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

Примечание: Я использую setTimeoutсо случайной задержкой в качестве универсального асинхронного функции, тот же самый пример относится к Ajax, readFile, onloadа также любые другие асинхронные потока.

Этот пример явно страдает от той же проблемы, что и другие примеры, он не ожидает выполнения асинхронной функции.

Давайте рассмотрим реализацию собственной системы обратного вызова. Во-первых, мы избавляемся от того уродства, outerScopeVarкоторое в этом случае совершенно бесполезно. Затем мы добавляем параметр, который принимает аргумент функции, наш обратный вызов. Когда асинхронная операция заканчивается, мы вызываем этот обратный вызов, передавая результат. Реализация (пожалуйста, прочитайте комментарии по порядку):

// 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);
}

Фрагмент кода из приведенного выше примера:

// 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);
}

Чаще всего в реальных случаях использования DOM API и большинство библиотек уже предоставляют функциональность обратного вызова ( helloCatAsyncреализация в этом демонстрационном примере). Вам нужно только передать функцию обратного вызова и понять, что она будет выполняться из синхронного потока, и реструктурировать свой код, чтобы приспособиться к этому.

Вы также заметите, что из-за асинхронного характера невозможно передать returnзначение из асинхронного потока обратно в синхронный поток, в котором был определен обратный вызов, поскольку асинхронные обратные вызовы выполняются задолго после того, как синхронный код уже завершен.

Вместо того, чтобы returnизвлекать значение из асинхронного обратного вызова, вам придется использовать шаблон обратного вызова или ... Обещания.

обещания

Несмотря на то, что с помощью vanilla JS существуют способы, позволяющие не допускать обратного вызова, популярность обещаний растет и в настоящее время стандартизируется в ES6 (см. Обещание - MDN ).

Обещания (иначе Futures) обеспечивают более линейное и, следовательно, приятное чтение асинхронного кода, но объяснение всей их функциональности выходит за рамки этого вопроса. Вместо этого я оставлю эти отличные ресурсы для заинтересованных:


Больше материалов для чтения об асинхронности JavaScript

  • Искусство Node - Callbacks очень хорошо объясняет асинхронный код и обратные вызовы с помощью ванильных примеров JS и кода Node.js.

Примечание. Я пометил этот ответ как вики сообщества, поэтому любой, имеющий не менее 100 репутаций, может редактировать и улучшать его! Пожалуйста, не стесняйтесь улучшать этот ответ или отправьте совершенно новый ответ, если хотите.

Я хочу превратить этот вопрос в каноническую тему, чтобы ответить на вопросы асинхронности, которые не связаны с Ajax (для этого есть ответ Как ответить на вызов AJAX? ), Поэтому эта тема нуждается в вашей помощи, чтобы быть как можно более полезной и полезной !

Fabrício Matté
источник
1
В вашем последнем примере, есть ли конкретная причина, по которой вы используете анонимные функции или она будет работать так же, используя именованные функции?
JDelage
1
Примеры кода немного странные, так как вы объявляете функцию после ее вызова. Работает, конечно, из-за подъема, но было ли это намеренно?
Берги
2
это тупик Феликс Клинг указывает на ваш ответ, а вы указываете на Феликс ответ
Махи
1
Вы должны понимать, что код красного круга является только асинхронным, потому что он выполняется NATIVE-асинхронными функциями JavaScript. Это особенность вашего движка JavaScript - будь то Node.js или браузер. Это асинхронно, потому что оно передается как «обратный вызов» функции, которая по сути является черным ящиком (реализовано в C и т. Д.). Для несчастного разработчика они асинхронны ... только потому, что. Если вы хотите написать свою собственную асинхронную функцию, вы должны взломать ее, отправив в SetTimeout (myfunc, 0). Должны ли вы сделать это? Еще одна дискуссия .... наверное нет.
Шон Андерсон
@Fabricio Я искал спецификацию, определяющую «> = 4ms зажим», но не смог ее найти - я нашел упоминание о похожем механизме (для зажима вложенных вызовов) в MDN - developer.mozilla.org/en-US/docs / Web / API /… - есть ли у кого-нибудь ссылка на правую часть спецификации HTML.
Себи
147

Ответ Фабрисио точен; но я хотел дополнить его ответ чем-то менее техническим, сосредоточенным на аналогии, чтобы помочь объяснить концепцию асинхронности .


Аналогия ...

Вчера работа, которую я выполнял, требовала информации от коллеги. Я позвонил ему; вот как прошел разговор:

Я : Привет, Боб, мне нужно знать, как мы побывали в баре на прошлой неделе. Джим хочет сообщить об этом, и вы единственный, кто знает подробности об этом.

Боб : Конечно, но это займет у меня около 30 минут?

Я : Это здорово, Боб. Позвони мне, когда получишь информацию!

В этот момент я повесил трубку. Поскольку мне нужна была информация от Боба, чтобы заполнить свой отчет, я оставил отчет и вместо этого пошел выпить кофе, а потом получил какое-то электронное письмо. 40 минут спустя (Боб медленно), Боб перезвонил и дал мне информацию, в которой я нуждался. На этом этапе я возобновил свою работу со своим отчетом, так как у меня была вся необходимая информация.


Представьте, если бы разговор пошел так, как это;

Я : Привет, Боб, мне нужно знать, как мы побывали в баре на прошлой неделе. Джим хочет сообщить об этом, и вы единственный, кто знает подробности об этом.

Боб : Конечно, но это займет у меня около 30 минут?

Я : Это здорово, Боб. Я подожду.

И я сидел там и ждал. И ждал. И ждал. На 40 минут. Ничего не делая, кроме ожидания. В конце концов, Боб дал мне информацию, мы повесили трубку, и я закончил свой отчет. Но я потерял 40 минут производительности.


Это асинхронное и синхронное поведение

Это именно то, что происходит во всех примерах в нашем вопросе. Загрузка изображения, загрузка файла с диска и запрос страницы через AJAX - все это медленные операции (в контексте современных вычислений).

Вместо ожидания завершения этих медленных операций JavaScript позволяет зарегистрировать функцию обратного вызова, которая будет выполнена после завершения медленной операции. Тем временем, однако, JavaScript продолжит выполнять другой код. Тот факт, что JavaScript выполняет другой код , ожидая завершения медленной операции, делает поведение асинхронным . Если бы JavaScript ожидал завершения операции перед выполнением любого другого кода, это было бы синхронным поведением.

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

В приведенном выше коде мы просим загрузить JavaScript lolcat.png, что является сложным процессом . Функция обратного вызова будет выполнена, как только эта медленная операция будет выполнена, но тем временем JavaScript продолжит обрабатывать следующие строки кода; то есть alert(outerScopeVar).

Вот почему мы видим предупреждение undefined; так как alert()обрабатывается сразу, а не после того, как изображение было загружено.

Чтобы исправить наш код, все, что нам нужно сделать, это переместить alert(outerScopeVar)код в функцию обратного вызова. Как следствие этого нам больше не нужна outerScopeVarпеременная, объявленная как глобальная переменная.

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

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

img.src = 'lolcat.png';

Вы всегда увидите, что обратный вызов указан как функция, потому что это единственный * способ в JavaScript определить некоторый код, но не выполнять его до тех пор, пока он не будет выполнен позже.

Следовательно, во всех наших примерах function() { /* Do something */ }это обратный вызов; чтобы исправить все примеры, все, что нам нужно сделать, это переместить туда код, который нуждается в ответе операции!

* Технически вы также можете использовать eval(), но eval()это зло для этой цели


Как мне заставить своего звонящего ждать?

В настоящее время вы можете иметь некоторый код, похожий на этот;

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

Однако теперь мы знаем, что это return outerScopeVarпроисходит немедленно; до того, как onloadфункция обратного вызова обновила переменную. Это приводит к getWidthOfImage()возвращению undefinedи undefinedпредупреждению.

Чтобы это исправить, нам нужно разрешить вызывающей функции getWidthOfImage()зарегистрировать обратный вызов, а затем переместить оповещение ширины в этот обратный вызов;

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

... как и прежде, обратите внимание, что мы смогли удалить глобальные переменные (в данном случае width).

Мэтт
источник
Но чем полезно оповещение или отправка на консоль, если вы хотите использовать результаты в другом расчете или сохранить его в переменной объекта?
Кен Ингрэм
68

Вот более краткий ответ для людей, которые ищут краткий справочник, а также некоторые примеры, использующие обещания и async / await.

Начните с наивного подхода (который не работает) для функции, которая вызывает асинхронный метод (в данном случае setTimeout) и возвращает сообщение:

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

undefinedрегистрируется в этом случае, потому что getMessageвозвращается до setTimeoutвызова и обновляется outerScopeVar.

Два основных способа решить эту проблему - использовать обратные вызовы и обещания :

Callbacks

Здесь изменение заключается в том, что он getMessageпринимает callbackпараметр, который будет вызываться для доставки результатов обратно в вызывающий код, когда он станет доступен.

function getMessage(callback) {
  setTimeout(function() {
    callback('Hello asynchronous world!');
  }, 0);
}
getMessage(function(message) {
  console.log(message);
});

обещания

Обещания предоставляют альтернативу, которая является более гибкой, чем обратные вызовы, поскольку их можно естественным образом объединить для координации нескольких асинхронных операций. Promises / A + стандартная реализация изначально предусмотрен в Node.js (0.12+) и многих современных браузерах, но также реализован в библиотеках , как Bluebird и 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 предоставляет функциональность, аналогичную обещаниям с его отложенными.

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

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

асинхронная / Await

Если в вашей среде JavaScript есть поддержка asyncи await(например, Node.js 7.6+), вы можете синхронно использовать обещания внутри asyncфункций:

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();
JohnnyHK
источник
Ваш образец на Обещаниях - в основном то, что я искал последние несколько часов. Ваш пример прекрасен и одновременно объясняет обещания. Почему это нигде не удивительно.
Винсент П
Это все хорошо, но что если вам нужно вызвать getMessage () с параметрами? Как бы вы написали выше в этом сценарии?
Chiwda
2
@Chiwda Вы просто поставить параметр обратного вызова в прошлом: function getMessage(param1, param2, callback) {...}.
JohnnyHK
Я пробую ваш async/awaitобразец, но у меня проблемы. Вместо того, чтобы создавать экземпляр new Promise, я делаю .Get()вызов и поэтому не имею доступа ни к какому resolve()методу. Таким образом, мой getMessage()возвращает обещание, а не результат. Не могли бы вы немного отредактировать свой ответ, чтобы показать рабочий синтаксис для этого?
InteXX
@InteXX Я не уверен, что ты имеешь в виду, когда .Get()звонишь. Наверное, лучше всего написать новый вопрос.
JohnnyHK
52

Чтобы заявить очевидное, чашка представляет outerScopeVar.

Асинхронные функции будут как ...

асинхронный вызов для кофе

Йоханнес Фаренкруг
источник
13
Принимая во внимание, что попытка заставить асинхронную функцию действовать синхронно, это будет попытка выпить кофе за 1 секунду, и вылить его на колени через 1 минуту.
Teepeemm
Если бы это указывало на очевидное, я не думаю, что вопрос был бы задан, нет?
брокколи2000
2
@ broccoli2000 Я не имел в виду, что вопрос был очевиден, но очевидно, что на рисунке изображена чашка :)
Йоханнес Фаренкруг,
13

Другие ответы превосходны, и я просто хочу дать прямой ответ на этот вопрос. Просто ограничение jQuery асинхронными вызовами

Все вызовы ajax (включая $.getили $.postили $.ajax) являются асинхронными.

Учитывая ваш пример

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

Выполнение кода начинается со строки 1, объявляет переменную и триггеры, а также асинхронный вызов в строке 2 (т. Е. После запроса) и продолжает выполнение со строки 3, не дожидаясь завершения запроса после завершения запроса.

Допустим, что почтовый запрос завершается за 10 секунд, значение outerScopeVarбудет установлено только после этих 10 секунд.

Попробовать,

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

Теперь, когда вы выполните это, вы получите предупреждение в строке 3. Теперь подождите некоторое время, пока вы не убедитесь, что почтовый запрос вернул какое-то значение. Затем, когда вы нажмете OK, в окне оповещений следующее оповещение выведет ожидаемое значение, потому что вы его ждали.

В реальной жизни код становится

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

Весь код, который зависит от асинхронных вызовов, перемещается внутри асинхронного блока или путем ожидания асинхронных вызовов.

Тея
источник
or by waiting on the asynchronous callsКак это сделать?
InteXX
@InteXX Используя метод обратного вызова
Теджа
У вас есть быстрый пример синтаксиса?
InteXX
10

Во всех этих сценариях outerScopeVarизменяется или присваивается значение асинхронно или происходит в более позднее время (ожидание или прослушивание какого-либо события), для которого текущее выполнение не будет ожидать. Так что во всех этих случаях текущий поток выполнения приводит кouterScopeVar = undefined

Давайте обсудим каждый пример (я отметил часть, которая вызывается асинхронно или с задержкой для некоторых событий):

1.

введите описание изображения здесь

Здесь мы регистрируем список событий, который будет выполняться при этом конкретном событии. Здесь происходит загрузка изображения. Затем текущее выполнение продолжается со следующими строками, img.src = 'lolcat.png';и в то alert(outerScopeVar);же время событие может не произойти. т.е. img.onloadожидание загрузки указанного изображения асинхронно. Это произойдет во всех следующих примерах - событие может отличаться.

2.

2

Здесь роль тайм-аута играет роль, которая вызовет обработчик по истечении указанного времени. Это так 0, но все же он регистрирует асинхронное событие, которое будет добавлено в последнюю позицию Event Queueдля выполнения, что обеспечивает гарантированную задержку.

3.

введите описание изображения здесь На этот раз обратный вызов ajax.

4.

введите описание изображения здесь

Узел может рассматриваться как король асинхронного кодирования. Здесь отмеченная функция зарегистрирована как обработчик обратного вызова, который будет выполнен после чтения указанного файла.

5.

введите описание изображения здесь

Очевидное обещание (что-то будет сделано в будущем) является асинхронным. см. В чем разница между отложенным, обещанием и будущим в JavaScript?

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

Том Себастьян
источник