Почему моя переменная не изменяется после того, как я изменяю ее внутри функции? - Ссылка на асинхронный код
Учитывая следующие примеры, почему не 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 . Не стесняйтесь улучшать этот вопрос и добавлять более упрощенные примеры, с которыми может идентифицировать себя сообщество.
Ответы
Ответ одним словом: асинхронность .
Предисловие
Эта тема повторялась как минимум пару тысяч раз здесь, в Stack Overflow. Поэтому прежде всего я хотел бы указать на некоторые чрезвычайно полезные ресурсы:
Ответ @Felix Kling на вопрос «Как мне вернуть ответ от асинхронного вызова?» . См. Его отличный ответ, объясняющий синхронные и асинхронные потоки, а также раздел «Код реструктуризации».
@Benjamin Gruenbaum также приложил много усилий для объяснения асинхронности в той же теме.Ответ @Matt Esch на "Получить данные из fs.readFile" также очень хорошо объясняет асинхронность простым способом.
Ответ на поставленный вопрос
Давайте сначала проследим общее поведение. Во всех примерах outerScopeVar
модифицируется внутри функции . Эта функция явно не выполняется немедленно, она назначается или передается в качестве аргумента. Это то, что мы называем обратным вызовом .
Теперь вопрос в том, когда вызывается этот обратный вызов?
Это зависит от случая. Попробуем еще раз проследить общее поведение:
img.onload
может быть вызван когда-нибудь в будущем , когда (и если) изображение будет успешно загружено.setTimeout
может быть вызван когда-нибудь в будущем , после того, как истечет задержка и время ожидания не будет отмененоclearTimeout
. Примечание: даже при использовании в0
качестве задержки все браузеры имеют минимальный предел задержки времени ожидания (указанный в спецификации HTML5 равным 4 мс).$.post
Обратный вызов jQuery может быть вызван когда-нибудь в будущем , когда (и если) запрос Ajax будет успешно выполнен.- Node.js
fs.readFile
может быть вызван когда-нибудь в будущем , когда файл будет успешно прочитан или возникнет ошибка.
Во всех случаях у нас есть обратный вызов, который может сработать когда-нибудь в будущем . Это «когда-нибудь в будущем» мы называем асинхронным потоком .
Асинхронное выполнение выталкивается из синхронного потока. То есть асинхронный код никогда не будет выполняться, пока выполняется стек синхронного кода. Это означает, что JavaScript является однопоточным.
В частности, когда JS-движок бездействует - не выполняет стек (а) синхронного кода - он будет опрашивать события, которые могли вызвать асинхронные обратные вызовы (например, истекший тайм-аут, полученный сетевой ответ), и выполнять их одно за другим. Это рассматривается как цикл событий .
То есть асинхронный код, выделенный нарисованными от руки красными фигурами, может выполняться только после того, как будет выполнен весь оставшийся синхронный код в соответствующих блоках кода:
Короче говоря, функции обратного вызова создаются синхронно, но выполняются асинхронно. Вы просто не можете полагаться на выполнение асинхронной функции, пока не узнаете, что она выполнилась, и как это сделать?
Это действительно просто. Логика, которая зависит от выполнения асинхронной функции, должна запускаться / вызываться изнутри этой асинхронной функции. Например, перемещение alert
s и 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 (см. Promise - 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
, что является медленной операцией. Функция обратного вызова будет выполнена после завершения этой медленной операции, но тем временем 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
.
Два основных способа решить эту проблему - использовать обратные вызовы и обещания :
Обратные вызовы
Изменение здесь заключается в том, что 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);
});
асинхронный / ожидание
Если ваша среда 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();
Чтобы заявить очевидное, чашка представляет собой outerScopeVar
.
Асинхронные функции похожи на ...
Другие ответы превосходны, и я просто хочу дать на это прямой ответ. Просто ограничение асинхронных вызовов 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);
});
Весь код, который зависит от асинхронных вызовов, перемещается внутри асинхронного блока или в ожидании асинхронных вызовов.
Во всех этих сценариях outerScopeVar
изменяется или присваивается значение асинхронно или происходит в более позднее время (ожидание или ожидание возникновения какого-либо события), для которого текущее выполнение не будет ждать. Таким образом, во всех этих случаях текущий поток выполнения приводит кouterScopeVar = undefined
Давайте обсудим каждый пример (я выделил часть, которая вызывается асинхронно или задерживается для возникновения каких-то событий):
1.
Здесь мы регистрируем список событий, который будет выполняться при этом конкретном событии. Здесь загрузка изображения. Затем текущее выполнение продолжается со следующими строками, img.src = 'lolcat.png';
и alert(outerScopeVar);
при этом событие может не произойти. т.е. функция img.onload
ожидания загрузки упомянутого изображения асинхронно. Это будет происходить в следующем примере - событие может отличаться.
2.
Здесь играет роль событие тайм-аута, которое вызовет обработчик по истечении указанного времени. Вот он 0
, но все же он регистрирует асинхронное событие, оно будет добавлено в последнюю позицию Event Queue
для выполнения, что обеспечивает гарантированную задержку.
3.
4.
Узел можно считать королем асинхронного кодирования. Здесь отмеченная функция регистрируется как обработчик обратного вызова, который будет выполнен после чтения указанного файла.
5.
Очевидное обещание (что-то будет сделано в будущем) асинхронное. см. В чем разница между Deferred, Promise и Future в JavaScript?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript