Відповідь одним словом: асинхронність .
Передмови
Ця тема була повторена щонайменше пару тисяч разів, тут, у стеку переповнення. Отже, спершу я хотів би зазначити кілька надзвичайно корисних ресурсів:
Відповідь на питання
Давайте спочатку простежимо загальну поведінку. У всіх прикладах 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);
}
Найчастіше в реальних випадках використання API DOM і більшість бібліотек вже забезпечують функцію зворотного дзвінка ( helloCatAsync
реалізація в цьому показовому прикладі). Вам потрібно лише передати функцію зворотного виклику і зрозуміти, що вона буде виконуватись із синхронного потоку та реструктуризувати ваш код, щоб уникнути цього.
Ви також помітите, що через асинхронний характер неможливо визначити return
значення від асинхронного потоку назад до синхронного потоку, де було визначено зворотний виклик, оскільки асинхронні зворотні виклики виконуються задовго після того, як синхронний код вже завершив виконання.
Замість того, return
щоб виводити значення з асинхронного зворотного дзвінка, вам доведеться використовувати шаблон зворотного дзвінка або ... Обіцянки.
Обіцянки
Хоча існують способи зберегти зворотний дзвінок у ванілі з ванільним JS, обіцянки зростають у популярності і в даний час стандартизовані в ES6 (див. Обіцяння - MDN ).
Обіцянки (aka Futures) забезпечують більш лінійне і, таким чином, приємне зчитування асинхронного коду, але пояснення всієї їх функціональності виходить за рамки цього питання. Натомість я залишу ці чудові ресурси для зацікавлених:
Більше читання матеріалів про асинхронність JavaScript
Примітка: я позначив цю відповідь спільнотою Wiki, тому кожен, хто має принаймні 100 репутацій, може редагувати та вдосконалювати її! Будь ласка, не соромтеся вдосконалити цю відповідь або надішліть абсолютно нову відповідь, якщо також хочете.
Я хочу перетворити це питання на канонічну тему, щоб відповісти на питання асинхронності, не пов'язані з Ajax (існує як повернути відповідь на дзвінок AJAX? Для цього), тому ця тема потребує вашої допомоги, щоб бути максимально доброю та корисною. !