Одним словом ответ: асинхронность .
Предисловия
Эта тема повторялась, по крайней мере, пару тысяч раз здесь, в Переполнении стека. Поэтому прежде всего я хотел бы указать на некоторые чрезвычайно полезные ресурсы:
Ответ на поставленный вопрос
Давайте сначала проследим общее поведение. Во всех примерах outerScopeVar
модификация внутри функции . Эта функция явно не выполняется сразу, она присваивается или передается в качестве аргумента. Это то, что мы называем обратным вызовом .
Теперь вопрос в том, когда вызывается этот обратный вызов?
Это зависит от случая. Давайте попробуем снова проследить какое-то общее поведение:
img.onload
может быть вызвано когда-нибудь в будущем , когда (и если) изображение будет успешно загружено.
setTimeout
может быть вызван когда-нибудь в будущем , после того, как задержка истекла, и тайм-аут не был отменен clearTimeout
. Примечание: даже при использовании в 0
качестве задержки все браузеры имеют минимальный предел времени ожидания (указанный в спецификации HTML5 равным 4 мс).
$.post
Обратный вызов jQuery может быть вызван когда-нибудь в будущем , когда (и если) запрос Ajax будет успешно выполнен.
- Node.js
fs.readFile
могут быть вызваны когда-нибудь в будущем , когда файл будет успешно прочитан или возникла ошибка.
Во всех случаях у нас есть обратный вызов, который может быть запущен когда-нибудь в будущем . Это «когда-нибудь в будущем» мы называем асинхронным потоком .
Асинхронное выполнение выталкивается из синхронного потока. То есть асинхронный код никогда не будет выполняться во время выполнения стека синхронного кода. Это означает, что JavaScript является однопоточным.
Более конкретно, когда механизм JS находится в режиме ожидания, не выполняя стек (a) синхронного кода, он будет запрашивать события, которые могли вызвать асинхронные обратные вызовы (например, истекло время ожидания, полученный сетевой ответ), и выполнять их один за другим. Это рассматривается как Event Loop .
Таким образом, асинхронный код, выделенный в нарисованных от руки красных формах, может выполняться только после того, как весь оставшийся синхронный код в их соответствующих кодовых блоках выполнен:
Короче говоря, функции обратного вызова создаются синхронно, но выполняются асинхронно. Вы просто не можете полагаться на выполнение асинхронной функции, пока не узнаете, что она выполнена, и как это сделать?
Это действительно просто. Логика, которая зависит от выполнения асинхронной функции, должна запускаться / вызываться из этой асинхронной функции. Например, перемещение 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
извлекать значение из асинхронного обратного вызова, вам придется использовать шаблон обратного вызова или ... Обещания.
обещания
Несмотря на то, что с помощью vanilla JS существуют способы, позволяющие не допускать обратного вызова, популярность обещаний растет и в настоящее время стандартизируется в ES6 (см. Обещание - MDN ).
Обещания (иначе Futures) обеспечивают более линейное и, следовательно, приятное чтение асинхронного кода, но объяснение всей их функциональности выходит за рамки этого вопроса. Вместо этого я оставлю эти отличные ресурсы для заинтересованных:
Больше материала для чтения об асинхронности JavaScript
- Искусство Node - Callbacks очень хорошо объясняет асинхронный код и обратные вызовы с помощью ванильных примеров JS и кода Node.js.
Примечание. Я пометил этот ответ как вики сообщества, поэтому любой, имеющий не менее 100 репутаций, может редактировать и улучшать его! Пожалуйста, не стесняйтесь улучшать этот ответ или отправьте совершенно новый ответ, если хотите.
Я хочу превратить этот вопрос в каноническую тему, чтобы ответить на вопросы асинхронности, которые не связаны с Ajax (для этого есть ответ Как ответить на вызов AJAX? ), Поэтому эта тема нуждается в вашей помощи, чтобы быть как можно более полезной и полезной !