Чому моя змінна не змінюється після того, як я змінив її всередині функції? - Асинхронна посилання на код


669

З огляду на наступні приклади, чому не 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 . Не соромтеся вдосконалити це питання та додайте більш спрощені приклади, з якими може визначитись громада.



@Dukeling спасибі, я майже впевнений, що я прокоментував це посилання, але, мабуть, є деякі коментарі, які відсутні. Крім того, щодо вашої редакції: я вважаю, що в заголовку "канонічність" та "асинхронність" допомагає під час пошуку цього питання позначити ще одне питання як обдурення. І звичайно, це також допомагає знайти це питання від Google, шукаючи пояснення асинхронності.
Фабрісіо Матте

3
Якщо додати трохи більше думки, "тема канонічної асинхронності" є дещо важкою за назвою, "посилання на асинхронний код" простіше і об'єктивніше. Я також вважаю, що більшість людей шукають "асинхронність" замість "асинхронність".
Fabrício Matté

1
Деякі люди ініціалізують свою змінну перед викликом функції. Як щодо зміни назви, яка так чи інакше представляє це? Як-от "Чому моя змінна не змінюється після того, як я модифікую її всередині функції?" ?
Фелікс Клінг

У всіх прикладах коду, які ви згадали вище, "сигнал (externalScopeVar);" виконує ЗАРАЗ, тоді як присвоєння значення "externalScopeVar" відбувається LATER (асинхронно).
рефактор

Відповіді:


542

Відповідь одним словом: асинхронність .

Передмови

Ця тема була повторена щонайменше пару тисяч разів, тут, у стеку переповнення. Отже, спершу я хотів би зазначити кілька надзвичайно корисних ресурсів:


Відповідь на питання

Давайте спочатку простежимо загальну поведінку. У всіх прикладах outerScopeVarфункція модифікується всередині функції . Ця функція явно виконується не відразу, вона призначається або передається як аргумент. Саме так ми називаємо зворотний дзвінок .

Тепер питання полягає в тому, коли викликається цей зворотний дзвінок?

Це залежить від випадку. Спробуємо знову простежити деяку загальну поведінку:

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

У всіх випадках у нас є зворотний дзвінок, який може запуститися колись у майбутньому . Це "колись у майбутньому" - це те, що ми називаємо асинхронним потоком .

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

Більш конкретно, коли двигун JS працює в режимі очікування - не виконуючи стек синхронного коду (а) - він опитуватиме події, які, можливо, викликали асинхронні зворотні виклики (наприклад, закінчився термін очікування, отриманий мережевий відповідь) та виконує їх один за одним. Це розглядається як цикл подій .

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

виділений код асинхронізації

Коротше кажучи, функції зворотного виклику створюються синхронно, але виконуються асинхронно. Ви просто не можете розраховувати на виконання асинхронної функції, поки не знаєте, що вона виконується, і як це зробити?

Це просто, справді. Логіка, яка залежить від виконання асинхронної функції, повинна бути запущена / викликана зсередини цієї асинхронної функції. Наприклад, переміщення alerts і console.logs всередині функції зворотного виклику призведе до очікуваного результату, оскільки результат доступний у цій точці.

Реалізація власної логіки зворотного виклику

Часто потрібно робити більше речей з результатом асинхронної функції або робити різні речі з результатом залежно від того, де викликана асинхронна функція. Розглянемо трохи складніший приклад:

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

Найчастіше в реальних випадках використання API DOM і більшість бібліотек вже забезпечують функцію зворотного дзвінка ( helloCatAsyncреалізація в цьому показовому прикладі). Вам потрібно лише передати функцію зворотного виклику і зрозуміти, що вона буде виконуватись із синхронного потоку та реструктуризувати ваш код, щоб уникнути цього.

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

Замість того, returnщоб виводити значення з асинхронного зворотного дзвінка, вам доведеться використовувати шаблон зворотного дзвінка або ... Обіцянки.

Обіцянки

Хоча існують способи утримати зворотний дзвінок у ванілі з ванільним JS, обіцянки зростають у популярності і в даний час стандартизовані в ES6 (див. Обіцяння - MDN ).

Обіцянки (aka Futures) забезпечують більш лінійне і, таким чином, приємне зчитування асинхронного коду, але пояснення всієї їх функціональності виходить за рамки цього питання. Натомість я залишу ці чудові ресурси для зацікавлених:


Більше читання матеріалів про асинхронність JavaScript

  • Мистецтво Node - CallBacks дуже добре пояснює асинхронний код та зворотні виклики, на прикладі JS ванілі та коду Node.js.

Примітка: я позначив цю відповідь як спільноту Wiki, тому кожен, хто має принаймні 100 репутацій, може редагувати та вдосконалювати її! Будь ласка, не соромтеся вдосконалити цю відповідь або надішліть абсолютно нову відповідь, якщо також хочете.

Я хочу перетворити це питання на канонічну тему, щоб відповісти на питання асинхронності, які не пов'язані з Ajax (є як повернути відповідь на дзвінок AJAX? Для цього), тому ця тема потребує вашої допомоги, щоб бути максимально доброю та корисною. !


1
У вашому останньому прикладі є конкретна причина, чому ви використовуєте анонімні функції, або це буде працювати так само, використовуючи названі функції?
JDelage

1
Приклади коду трохи дивні, оскільки ви оголошуєте функцію після її виклику. Працює через підйом, звичайно, але це було навмисно?
Бергі

2
це тупик. felix kling вказує на вашу відповідь, і ви вказуєте на відповідь felix
Mahi

1
Вам потрібно зрозуміти, що код червоного кола - це лише асинхронізація, оскільки він виконується функціями NATIVE async javascript. Це особливість вашого механізму javascript - будь то Node.js чи браузер. Це асинхронізація, оскільки вона передається як "зворотний виклик" функції, яка по суті є чорною скринькою (реалізована в C тощо). До нещасного розробника вони асинхронізуються ... просто тому. Якщо ви хочете написати власну функцію асинхронізації, вам потрібно зламати її, відправивши її в SetTimeout (myfunc, 0). Ви повинні це зробити? Ще одна дискусія .... напевно, ні.
Шон Андерсон

@Fabricio Я шукав специфікацію, що визначає "> = 4ms затискач", але не зміг її знайти - я знайшов згадку про подібний механізм (для затискання вкладених викликів) на MDN - developer.mozilla.org/en-US/docs / Web / API /… - чи має хто-небудь посилання на праву частину специфікації HTML.
Себі

147

Відповідь Фабріціо є місцем; але я хотів доповнити його відповідь чимось менш технічним, який зосереджується на аналогії, щоб допомогти пояснити поняття асинхронності .


Аналогія ...

Вчора робота, яку я робив, вимагала від колеги певної інформації. Я подзвонив йому; ось як пройшла розмова:

Я : Привіт Боб, мені потрібно знати, як ми минулого тижня працювали в барі . Джим хоче доповісти про це, а ти єдиний, хто знає про нього деталі.

Боб : Звичайно, але це займе 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).


Але як сповіщення або надсилання в консоль корисно, якщо ви хочете використовувати результати в іншому обчисленні або зберігати їх у змінній об'єкта?
Кен Інграм

68

Ось більш коротка відповідь для людей, які шукають швидку довідку, а також кілька прикладів із використанням обіцянок та асинхронізації / очікування.

Почніть з наївного підходу (який не працює) для функції, яка викликає асинхронний метод (в даному випадку 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

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();

Ваш зразок Обіцянок - це в основному те, що я шукав протягом останніх кількох годин. Ваш приклад прекрасний і одночасно пояснює Обіцянки. Чому цього ніде більше не буває.
Вінсент П

Це все добре, але що робити, якщо вам потрібно викликати getMessage () з параметрами? Як би ви написали сказане в тому сценарії?
Chiwda

2
@Chiwda Ви просто поставити параметр зворотного виклику в минулому: function getMessage(param1, param2, callback) {...}.
JohnnyHK

Я пробую ваш async/awaitзразок, але у мене виникають проблеми. Замість того, щоб створювати інстанцію new Promise, я .Get()телефоную, і тому не маю доступу до жодного resolve()методу. Таким чином, мій getMessage()повертає Обіцянку, а не результат. Чи можете ви трохи відредагувати свою відповідь, щоб відобразити для цього працюючий синтаксис?
InteXX

@InteXX Я не впевнений, що ти маєш на увазі під час .Get()дзвінка. Напевно, найкраще розмістити нове запитання.
JohnnyHK

52

Стверджуючи очевидне, чашка являє собою outerScopeVar.

Асинхронні функції схожі на ...

асинхронний дзвінок на каву


13
У той час як спроба зробити асинхронну функцію синхронно буде намагатися випити кави за 1 секунду, і вона вилила вам у колінах за 1 хвилину.
Teepeemm

Якби це було очевидним, я не думаю, що це питання було б задане, ні?
broccoli2000

2
@ broccoli2000 Я не мав на увазі, що питання було очевидним, але що очевидно, що представляє чашка на малюнку :)
Йоханнес Фаренкруг

13

Інші відповіді відмінні, і я просто хочу надати пряму відповідь на це. Просто обмеження асинхронних дзвінків 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);
});

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


or by waiting on the asynchronous callsЯк це зробити?
InteXX

@InteXX Використовуючи метод зворотного дзвінка
Тея

Чи є у вас швидкий приклад синтаксису?
InteXX

10

У всіх цих сценаріях outerScopeVarзмінюється або присвоюється значення асинхронно або відбувається в більш пізній час (очікування або прослуховування певної події), для якого поточне виконання не чекатиме . Отже, у всіх цих випадках поточний потік виконання призводить доouterScopeVar = undefined

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

1.

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

Тут ми реєструємо список подій, який буде виконуватися в рамках конкретної події. Тут завантажується зображення. Потім поточне виконання триває з наступними рядками, img.src = 'lolcat.png';а alert(outerScopeVar);тим часом подія може не відбутися. тобто функція img.onloadчекає завантаження згаданого зображення, асинхронно. Це відбудеться у всіх наступних прикладах - подія може відрізнятися.

2.

2

Тут відіграє роль подія тайм-аута, яка викличе обробник через вказаний час. Ось він 0, але все ж він реєструє асинхронну подію, він буде доданий до останньої позиції Event Queueдля виконання, що робить гарантовану затримку.

3.

введіть тут опис зображення Цього разу зворотний виклик Ajax.

4.

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

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

5.

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

Очевидна обіцянка (щось буде зроблено в майбутньому) є асинхронною. див. Які відмінності між відкладеним, обіцяним та майбутнім у JavaScript?

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.