Почему моя переменная не изменяется после того, как я изменяю ее внутри функции? - Асинхронный код
567 Fabrício Matté [2014-05-15 02:55:00]
Учитывая следующие примеры, почему во всех случаях outerScopeVar
undefined?
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. Не стесняйтесь улучшать этот вопрос и добавлять более упрощенные примеры, с которыми сообщество может идентифицироваться.
javascript asynchronous
6 ответов
459 Решение Fabrício Matté [2014-05-15 02:55:00]
Один ответ на слово: асинхронность.
Предисловия
Эта тема была повторена по крайней мере пару тысяч раз, здесь, в Stack Overflow. Следовательно, во-первых, я хотел бы указать на некоторые чрезвычайно полезные ресурсы:
-
@Felix Kling "Как вернуть ответ от вызова AJAX". См. Его отличный ответ, объясняющий синхронные и асинхронные потоки, а также раздел "Реструктуризация кода".
@Benjamin Gruenbaum также приложил много усилий, объясняя асинхронность в той же теме. -
Ответ @Matt Esch на "Получить данные из fs.readFile" также объясняет асинхронность очень хорошо простым способом.
Ответ на поставленный вопрос
Пусть сначала проследит общее поведение. Во всех примерах outerScopeVar
изменяется внутри функции. Эта функция явно не выполняется немедленно, она назначается или передается в качестве аргумента. Это то, что мы называем обратным вызовом.
Теперь вопрос в том, когда называется обратный вызов?
Это зависит от случая. Попробуйте еще раз проследить некоторое общее поведение:
-
img.onload
можно назвать когда-то в будущем, когда (и если) изображение успешно загружено. -
setTimeout
можно назвать когда-то в будущем, после истечения задержки, и таймаут не был отмененclearTimeout
. Примечание: даже при использовании0
качестве задержки все браузеры имеют ограничение на минимальную задержку (указано в 4 мс в спецификации HTML5). - Вызов jQuery
$.post
может быть вызван в будущем, когда (и если) запрос Ajax был успешно завершен. - Node.js
fs.readFile
может быть вызван в будущем, когда файл был успешно прочитан или возникла ошибка.
Во всех случаях у нас есть обратный вызов, который может быть запущен в будущем. Это "когда-то в будущем" - это то, что мы называем асинхронным потоком.
Асинхронное выполнение выталкивается из синхронного потока. То есть, асинхронный код никогда не будет выполняться во время выполнения стека синхронного кода. Это означает, что JavaScript является однопоточным.
Более конкретно, когда движок JS не работает - не выполняет стек (a) синхронного кода - он будет опросить события, которые могут вызвать асинхронные обратные вызовы (например, истекший тайм-аут, принятый сетевой ответ) и выполнить их один за другим. Это рассматривается как Event Loop.
То есть, асинхронный код, выделенный в рисованных красных фигурах, может выполняться только после того, как все оставшийся синхронный код в их соответствующих блоках кода выполнил:
Короче говоря, функции обратного вызова создаются синхронно, но выполняются асинхронно. Вы просто не можете полагаться на выполнение асинхронной функции, пока не знаете, что она выполнила, и как это сделать?
Это просто, на самом деле. Логику, зависящую от выполнения асинхронной функции, следует запускать/вызывать изнутри этой асинхронной функции. Например, перемещение alert
и console.log
тоже внутри функции обратного вызова выводит ожидаемый результат, потому что результат доступен в этой точке.
Реализация собственной логики обратного вызова
Часто вам нужно делать больше вещей с результатом асинхронной функции или делать разные вещи с результатом в зависимости от того, где была вызвана асинхронная функция. Давайте рассмотрим несколько более сложный пример:
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);
}
Чаще всего в реальных случаях API DOM и большинство библиотек уже предоставляют функции обратного вызова (реализация helloCatAsync
в этом демонстрационном примере). Вам нужно только передать функцию обратного вызова и понять, что она будет выполняться из синхронного потока, и перестроить ваш код, чтобы он соответствовал этому.
Вы также заметите, что из-за асинхронного характера невозможно return
значение из асинхронного потока обратно в синхронный поток, где был определен обратный вызов, поскольку асинхронные обратные вызовы выполняются задолго после того, как синхронный код уже завершил выполнение.
Вместо того, чтобы return
значение из асинхронного обратного вызова, вам придется использовать шаблон обратного вызова или... Обещать.
обещания
Хотя есть способы сохранить обратного вызова ад в страхе с ванильным JS, обещает все большую популярность и в настоящее время стандартизированы в ES6 (см Promise - MDN).
Обещания (aka Futures) обеспечивают более линейное и, таким образом, приятное чтение асинхронного кода, но объяснение всей их функциональности выходит за рамки этого вопроса. Вместо этого я оставлю эти превосходные ресурсы для заинтересованных:
Дополнительные материалы для чтения об асинхронности JavaScript
- Art of Node - Callbacks объясняет асинхронный код и обратные вызовы очень хорошо с примерами ванили JS и кодом Node.js.
Примечание. Я отметил этот ответ как Community Wiki, поэтому любой, кто имеет не менее 100 репутаций, может редактировать и улучшать его! Пожалуйста, не стесняйтесь, чтобы улучшить этот ответ или представить совершенно новый ответ, если хотите.
Я хочу превратить этот вопрос в каноническую тему, чтобы ответить на вопросы асинхронности, которые не связаны с Ajax (есть вопрос, как вернуть ответ от вызова AJAX?), Поэтому эта тема нуждается в вашей помощи, чтобы быть как можно более полезной и полезной !
127 Matt [2014-05-29 12:09:00]
Ответ Fabrício - на месте; но я хотел дополнить его ответ чем-то менее техническим, который фокусируется на аналогии, чтобы помочь объяснить концепцию асинхронности.
Аналогия...
Вчера работа, которую я делал, требовала некоторой информации от коллеги. Я позвонил ему; вот как пошла беседа:
Me: Привет, Боб, мне нужно знать, как мы foo'd bar'd на прошлой неделе. Джим хочет сообщить об этом, и вы единственный, кто знает подробности об этом.
Боб: Конечно, но мне понадобится около 30 минут?
Я: Этот великий Боб. Верните мне кольцо, когда у вас есть информация!
В этот момент я повесил трубку. Поскольку мне нужна была информация от Боба, чтобы закончить мой отчет, я оставил отчет и пошел за кофе вместо этого, затем я догнал какой-то адрес электронной почты. 40 минут спустя (Боб медленно), Боб позвонил и дал мне информацию, в которой я нуждался. На этом этапе я возобновил свою работу с моим докладом, поскольку у меня была вся необходимая мне информация.
Представьте себе, если бы разговор шел именно так;
Me: Привет, Боб, мне нужно знать, как мы foo'd bar'd на прошлой неделе. Джим хочет сообщить об этом, и ты единственный, кто знает подробности об этом.
Боб: Конечно, но мне понадобится около 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)
код alert(outerScopeVar)
в функцию обратного вызова. Вследствие этого нам больше не нужна переменная outerScopeVar
объявленная как глобальная переменная.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Вы всегда увидите, что обратный вызов указан как функция, потому что единственный способ JavaScript в 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
).
57 JohnnyHK [2015-01-21 02:42:00]
Здесь более краткий ответ для людей, которые ищут быструю ссылку, а также некоторые примеры, использующие обещания и асинхронные/ждущие.
Начните с наивного подхода (который не работает) для функции, вызывающей асинхронный метод (в данном случае 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 Отложенные
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();
48 Johannes Fahrenkrug [2015-12-08 19:48:00]
11 Teja [2016-02-26 06:59:00]
Другие ответы превосходны, и я просто хочу дать прямой ответ на этот вопрос. Просто ограничение асинхронных вызовов 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);
});
Весь код, зависящий от асинхронных вызовов, перемещается внутри асинхронного блока или ожидает асинхронных вызовов.
8 Tom Sebastian [2015-10-27 09:35:00]
Во всех этих сценариях outerScopeVar
изменяется или назначается значение асинхронно или , происходящее в более позднее время (ожидание или прослушивание какого-либо события), для которого текущее исполнение будет не ждите. Таким образом, во всех этих случаях текущий поток выполнения приводит к outerScopeVar = undefined
Давайте обсудим каждый пример (я отметил часть, которая называется асинхронно или задерживается для возникновения некоторых событий):
1.
Здесь мы регистрируем eventlistner, который будет выполняться на этом конкретном событии. Загрузите изображение. Затем текущее выполнение продолжается следующими строками img.src = 'lolcat.png';
и alert(outerScopeVar);
, тем временем событие может не произойти. т.е. funtion img.onload
ожидают, что упомянутое изображение загрузится, как можно скорее. Это произойдет во всех следующих случаях: событие может отличаться.
2.
Здесь играет роль время ожидания, которое вызывается обработчиком по истечении указанного времени. Здесь 0
, но он регистрирует асинхронное событие, которое будет добавлено в последнюю позицию Event Queue
для выполнения, что делает гарантированную задержку.
3.
4.
Node можно рассматривать как король асинхронного кодирования. Если отмеченная функция зарегистрирована как обработчик обратного вызова, которая будет выполнена после прочтения указанного файла.
5.
Очевидное обещание (что-то будет сделано в будущем) является асинхронным. см. Каковы различия между Отсрочкой, обещанием и Будущим в JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript