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

Учитывая следующие примеры, почему во всех случаях 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. Не стесняйтесь улучшать этот вопрос и добавлять более упрощенные примеры, с которыми сообщество может идентифицироваться.

Ответы

Ответ 1

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

Предисловия

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


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

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

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

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

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

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

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

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

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

async code highlighted

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

Это действительно просто. Логика, которая зависит от выполнения асинхронной функции, должна запускаться/вызываться из этой асинхронной функции. Например, при перемещении 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);
}

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

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

Ответ 2

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


Аналогия...

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

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

Боб: Конечно, но это займет у меня около 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).

Ответ 3

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

Начните с наивного подхода (который не работает) для функции, вызывающей асинхронный метод (в данном случае 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();

Ответ 4

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

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

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

Ответ 5

Другие ответы превосходны, и я просто хочу дать прямой ответ на этот вопрос. Просто ограничение асинхронных вызовов 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);
});

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

Ответ 6

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

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

1.

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

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

2.

2

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

3.

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

4.

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

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

5.

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

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

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

Ответ 7

setTimeout(functionName, 0) // with emphasis on 0

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