Почему моя переменная не изменилась после того, как я изменил ее внутри функции? - асинхронная ссылка на код
Учитывая следующие примеры, почему 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. Не стесняйтесь улучшать этот вопрос и добавлять более упрощенные примеры, с которыми сообщество может идентифицировать.
7 ответов
Одним словом ответ: асинхронность.
Предисловия
Эта тема повторялась, по крайней мере, пару тысяч раз здесь, в Переполнении стека. Поэтому прежде всего я хотел бы указать на некоторые чрезвычайно полезные ресурсы:
@Felix Kling's "Как вернуть ответ от вызова AJAX". Посмотрите его превосходный ответ, объясняющий синхронные и асинхронные потоки, а также раздел "Реструктуризация кода".
@ Бенджамин Грюнбаум также приложил много усилий для объяснения асинхронности в той же теме.Ответ @Matt Esch "Получить данные из fs.readFile" также очень просто объясняет асинхронность.
Ответ на поставленный вопрос
Давайте сначала проследим общее поведение. Во всех примерах outerScopeVar
модифицируется внутри функции. Эта функция явно не выполняется сразу, она присваивается или передается в качестве аргумента. Это то, что мы называем обратным вызовом.
Теперь вопрос в том, когда вызывается этот обратный вызов?
Это зависит от случая. Давайте попробуем снова проследить некоторое общее поведение:
img.onload
может быть вызвано когда-нибудь в будущем, когда (и если) изображение будет успешно загружено.setTimeout
может быть вызван когда-нибудь в будущем, после того, как задержка истекла, и тайм-аут не был отмененclearTimeout
, Примечание: даже при использовании0
в качестве задержки все браузеры имеют минимальную задержку тайм-аута (в спецификации HTML5 она составляет 4 мс).- JQuery
$.post
Обратный вызов может быть вызван когда-нибудь в будущем, когда (и если) Ajax-запрос будет успешно выполнен. - Node.js-х
fs.readFile
может быть вызвано когда-нибудь в будущем, когда файл был успешно прочитан или выдана ошибка.
Во всех случаях у нас есть обратный вызов, который может быть запущен когда-нибудь в будущем. Это "когда-нибудь в будущем" мы называем асинхронным потоком.
Асинхронное выполнение выталкивается из синхронного потока. То есть, асинхронный код никогда не будет выполняться, пока выполняется стек синхронного кода. Это означает, что JavaScript является однопоточным.
Более конкретно, когда механизм JS бездействует - не выполняет стек (a) синхронного кода - он будет запрашивать события, которые могли вызвать асинхронные обратные вызовы (например, истек тайм-аут, полученный сетевой ответ), и выполнять их один за другим. Это рассматривается как Event Loop.
То есть асинхронный код, выделенный в нарисованных от руки красных формах, может выполняться только после того, как весь оставшийся синхронный код в их соответствующих кодовых блоках выполнен:
Короче говоря, функции обратного вызова создаются синхронно, но выполняются асинхронно. Вы просто не можете полагаться на выполнение асинхронной функции, пока не узнаете, что она выполнена, и как это сделать?
Это действительно просто. Логика, которая зависит от выполнения асинхронной функции, должна запускаться / вызываться изнутри этой асинхронной функции. Например, перемещение alert
с и console.log
s тоже внутри функции обратного вызова выдаст ожидаемый результат, потому что результат доступен в этой точке.
Реализация собственной логики обратного вызова
Часто вам нужно делать больше вещей с результатом асинхронной функции или делать разные вещи с результатом в зависимости от того, где была вызвана асинхронная функция. Давайте рассмотрим немного более сложный пример:
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
Используя значение из асинхронного обратного вызова, вы должны будете использовать шаблон обратного вызова или... Обещания.
обещания
Несмотря на то, что с ванильным JS есть способы не допускать обратного вызова в ад, обещания растут в популярности и в настоящее время стандартизируются в ES6 (см. Обещание - MDN).
Обещания (иначе Futures) обеспечивают более линейное и, следовательно, приятное чтение асинхронного кода, но объяснение всей их функциональности выходит за рамки этого вопроса. Вместо этого я оставлю эти отличные ресурсы для заинтересованных:
Больше материала для чтения об асинхронности JavaScript
- Искусство Node - Callbacks очень хорошо объясняет асинхронный код и обратные вызовы с помощью ванильных примеров JS и кода Node.js.
Примечание. Я пометил этот ответ как вики-сообщество, поэтому любой, имеющий не менее 100 репутаций, может редактировать и улучшать его! Пожалуйста, не стесняйтесь улучшать этот ответ или отправьте совершенно новый ответ, если хотите.
Я хочу превратить этот вопрос в каноническую тему, чтобы ответить на вопросы асинхронности, не связанные с Ajax (для этого есть ответ Как ответить на вызов AJAX?), Поэтому эта тема нуждается в вашей помощи, чтобы быть как можно более полезной и полезной!
Ответ Фабрисио точен; но я хотел дополнить его ответ чем-то менее техническим, сосредоточенным на аналогии, чтобы помочь объяснить концепцию асинхронности.
Аналогия...
Вчера работа, которую я выполнял, требовала информации от коллеги. Я позвонил ему; вот как прошел разговор:
Я: Привет, Боб, мне нужно знать, как мы побывали в баре на прошлой неделе. Джим хочет сообщить об этом, и вы единственный, кто знает подробности об этом.
Боб: Конечно, но это займет у меня около 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
, который является sloooow операции. Функция обратного вызова будет выполнена после выполнения этой медленной операции, но в то же время 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
).
Вот более краткий ответ для людей, которые ищут краткий справочник, а также некоторые примеры, использующие обещания и 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();
Другие ответы превосходны, и я просто хочу дать прямой ответ на этот вопрос. Просто ограничение асинхронными вызовами 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. Теперь подождите некоторое время, пока вы не убедитесь, что почтовый запрос вернул какое-то значение. Затем, когда вы нажмете ОК, в окне оповещения следующее оповещение выведет ожидаемое значение, потому что вы его ждали.
В реальной жизни код становится
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Весь код, который зависит от асинхронных вызовов, перемещается внутри асинхронного блока или путем ожидания асинхронных вызовов.
Во всех этих сценариях outerScopeVar
изменяется или присваивается значение асинхронно или происходит в более позднее время (ожидание или прослушивание какого-либо события), для которого текущее выполнение не будет ожидать. Так что во всех этих случаях текущий поток выполнения приводит к outerScopeVar = undefined
Давайте обсудим каждый пример (я отметил часть, которая вызывается асинхронно или с задержкой для некоторых событий):
1.
Здесь мы регистрируем список событий, который будет выполняться при этом конкретном событии. Здесь происходит загрузка изображения. Затем текущее выполнение продолжается со следующими строками. img.src = 'lolcat.png';
а также alert(outerScopeVar);
Между тем событие может не произойти. т.е. функция img.onload
дождитесь загрузки указанного изображения, асинхронно. Это произойдет во всех следующих примерах - событие может отличаться.
2.
Здесь роль тайм-аута играет роль, которая вызовет обработчик по истечении указанного времени. Вот 0
, но все же он регистрирует асинхронное событие, которое будет добавлено к последней позиции Event Queue
за исполнение, что делает гарантированную задержку.
3.
На этот раз обратный вызов ajax.
4.
Узел может рассматриваться как король асинхронного кодирования. Здесь отмеченная функция зарегистрирована как обработчик обратного вызова, который будет выполнен после чтения указанного файла.
5.
Очевидное обещание (что-то будет сделано в будущем) является асинхронным. см. В чем разница между отложенным, обещанием и будущим в JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript
Короткий ответ: асинхронность.
Зачем нужен асинхронный?
JavaScript является однопоточным, а это означает, что два фрагмента сценария не могут выполняться одновременно; они должны бежать один за другим. В браузерах JavaScript разделяет поток с множеством других вещей, которые отличаются от браузера к браузеру. Но обычно JavaScript находится в той же очереди, что и рисование, обновление стилей и обработка действий пользователя (например, выделение текста и взаимодействие с элементами управления формы). Активность в одной из этих вещей задерживает другие.
Вы, вероятно, использовали события и обратные вызовы, чтобы обойти это. Вот события: