→ Для більш загального пояснення поведінки асинхронізації з різними прикладами див . - Асинхронна посилання на код
→ Якщо ви вже розумієте проблему, перейдіть до можливих варіантів рішення нижче.
Проблема
У Ajax означає асинхронний . Це означає, що відправлення запиту (а точніше отримання відповіді) виводиться з нормального потоку виконання. У вашому прикладі, повертається негайно та наступне твердження, виконується до того, як функція, яку ви передали як зворотний виклик, навіть була викликана.$.ajax
return result;
success
Ось аналогія, яка, сподіваємось, робить різницю між синхронним та асинхронним потоком яснішою:
Синхронний
Уявіть, що ви телефонуєте другові і попросіть його подивитися щось на вас. Хоча це може зайняти деякий час, ви зачекаєтесь по телефону і зазираєте в космос, поки ваш друг не дасть вам відповіді, яка вам потрібна.
Те саме відбувається під час виклику функції, що містить "звичайний" код:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Незважаючи на те, що для виконання findItem
може знадобитися тривалий час, будь-який код, що настає після var item = findItem();
, повинен зачекати, поки функція поверне результат.
Асинхронний
Ви знову дзвоните своєму другові з тієї ж причини. Але цього разу ти кажеш йому, що ти поспішаєш, і він повинен передзвонити вам на мобільний телефон. Ви повісьте, вийдете з дому і зробіть все, що планували зробити. Як тільки ваш друг передзвонить вам, ви маєте справу з інформацією, яку він вам передав.
Саме це і відбувається, коли ви робите запит на Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Замість того, щоб чекати відповіді, виконання продовжується негайно, а заява після виклику Ajax виконується. Щоб отримати відповідь у підсумку, ви надаєте функцію, яку потрібно викликати, як тільки відповідь отримана, зворотний виклик (помічаєте щось? Передзвоніть ?). Будь-яка заява, що надходить після цього дзвінка, виконується до виклику зворотного дзвінка.
Рішення (и)
Отримати асинхронний характер JavaScript! Хоча певні асинхронні операції забезпечують синхронні аналоги (як це робить "Ajax"), як правило, не рекомендується їх використовувати, особливо в контексті браузера.
Чому це погано запитаєте ви?
JavaScript працює в потоці користувальницького інтерфейсу браузера, і будь-який тривалий процес заблокує інтерфейс користувача, зробивши його невідповідним. Крім того, існує верхня межа часу виконання JavaScript і браузер запитає користувача, продовжувати виконання чи ні.
Все це дійсно поганий досвід користувача. Користувач не зможе сказати, чи все працює добре чи ні. Крім того, ефект буде гіршим для користувачів з повільним зв’язком.
Далі ми розглянемо три різні рішення, які будуються один на одного:
- Обіцяє
async/await
(ES2017 +, доступний у старих браузерах, якщо ви використовуєте транспілятор або регенератор)
- Зворотні виклики (популярні у вузлі)
- Обіцянки з
then()
(ES2015 +, доступний у старих браузерах, якщо ви використовуєте одну з багатьох бібліотек з обіцянками)
Усі три доступні в поточних браузерах та на вузлі 7+.
Версія ECMAScript, випущена в 2017 році, представила підтримку на рівні синтаксису для асинхронних функцій. За допомогою async
і await
, ви можете писати асинхронно в "синхронному стилі". Код все ще асинхронний, але його легше читати / розуміти.
async/await
будується на основі обіцянок: async
функція завжди повертає обіцянку. await
"розгортає" обіцянку і або призводить до значення, з яким обіцянку було вирішено, або видає помилку, якщо обіцянку було відхилено.
Важливо: Ви можете використовувати лише await
всередині async
функції. Наразі верхній рівень await
ще не підтримується, тому вам, можливо, доведеться зробити асинхронний IIFE ( негайно викликаний вираз функції ) для запуску async
контексту.
Ви можете прочитати більше про async
та await
на MDN.
Ось приклад, який базується на затримці вище:
// Using 'superagent' which will return a promise.
var superagent = require('superagent')
// This is isn't declared as `async` because it already returns a promise
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
async function getAllBooks() {
try {
// GET a list of book IDs of the current user
var bookIDs = await superagent.get('/user/books');
// wait for 3 seconds (just for the sake of this example)
await delay();
// GET information about each book
return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
} catch(error) {
// If any of the awaited promises was rejected, this catch block
// would catch the rejection reason
return null;
}
}
// Start an IIFE to use `await` at the top level
(async function(){
let books = await getAllBooks();
console.log(books);
})();
Поточна версія браузера та вузлів підтримується async/await
. Ви також можете підтримувати старіші середовища, перетворюючи код на ES5 за допомогою регенератора (або інструментів, що використовують регенератор, наприклад Babel ).
Нехай функції приймають зворотні дзвінки
Зворотний виклик - це просто функція, передана іншій функції. Ця інша функція може викликати передану функцію, коли вона готова. У контексті асинхронного процесу зворотний виклик буде викликатися щоразу, коли буде виконано асинхронний процес. Зазвичай результат передається до зворотного дзвінка.
У прикладі запитання ви можете foo
прийняти зворотний дзвінок і використовувати його як success
зворотний дзвінок. Так це
var result = foo();
// Code that depends on 'result'
стає
foo(function(result) {
// Code that depends on 'result'
});
Тут ми визначили функцію "inline", але ви можете передати будь-яку посилання на функцію:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
foo
Сам визначається так:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
буде стосуватися функції, яку ми передаємо, foo
коли ми її викликаємо, і ми просто передаємо її success
. Тобто, коли запит Ajax буде успішним, $.ajax
зателефонує callback
та передасть відповідь на зворотний дзвінок (на який можна посилатисяresult
, оскільки саме так ми визначили зворотний виклик).
Ви також можете обробити відповідь перед тим, як передати її зворотному дзвінку:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
Простіше писати код за допомогою зворотних дзвінків, ніж може здатися. Зрештою, JavaScript у браузері сильно керується подіями (події DOM). Отримання відповіді «Аякс» - це не що інше, як подія.
Труднощі можуть виникнути, коли вам доведеться працювати зі стороннім кодом, але більшість проблем можна вирішити, просто продумавши потік додатків.
ES2015 +: Обіцяє тоді ()
Promise API є новою функцією ECMAScript 6 (ES2015), але вона має хорошу підтримку браузера вже. Існує також багато бібліотек, які реалізують стандартний API Promises і надають додаткові методи для полегшення використання та композиції асинхронних функцій (наприклад, bluebird ).
Обіцянки є контейнерами для майбутніх цінностей. Коли обіцянка отримує значення (воно вирішено ) або коли воно скасовується ( відхиляється ), воно повідомляє всіх своїх «слухачів», які хочуть отримати доступ до цього значення.
Перевага перед простими зворотними викликами полягає в тому, що вони дозволяють роз'єднати код і їх легше складати.
Ось простий приклад використання обіцянки:
function delay() {
// `delay` returns a promise
return new Promise(function(resolve, reject) {
// Only `delay` is able to resolve or reject the promise
setTimeout(function() {
resolve(42); // After 3 seconds, resolve the promise with value 42
}, 3000);
});
}
delay()
.then(function(v) { // `delay` returns a promise
console.log(v); // Log the value once it is resolved
})
.catch(function(v) {
// Or do something else if it is rejected
// (it would not happen in this example, since `reject` is not called).
});
Застосовуючи наш дзвінок у Ajax, ми могли використовувати такі обіцянки:
function ajax(url) {
return new Promise(function(resolve, reject) {
var xhr = new XMLHttpRequest();
xhr.onload = function() {
resolve(this.responseText);
};
xhr.onerror = reject;
xhr.open('GET', url);
xhr.send();
});
}
ajax("/echo/json")
.then(function(result) {
// Code depending on result
})
.catch(function() {
// An error occurred
});
Опис усіх переваг, які обіцяє пропозиція, виходить за межі цієї відповіді, але якщо ви пишете новий код, ви повинні серйозно їх розглянути. Вони забезпечують велику абстракцію та розділення вашого коду.
Більше інформації про обіцянки: HTML5 скелі - Обіцяння JavaScript
Бічна примітка: відкладені об'єкти jQuery
Відкладені об'єкти - це власна реалізація обіцянок jQuery (до того, як API Promise був стандартизований). Вони поводяться майже як обіцянки, але виявляють дещо інший API.
Кожен метод jQuery Ajax вже повертає "відкладений об'єкт" (фактично обіцянку відкладеного об'єкта), який ви можете просто повернути зі своєї функції:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Побічна примітка: Обіцяйте гатчі
Майте на увазі, що обіцянки та відкладені об’єкти - це лише контейнери для майбутнього значення, вони не є цінністю самі по собі. Наприклад, припустимо, у вас було таке:
function checkPassword() {
return $.ajax({
url: '/password',
data: {
username: $('#username').val(),
password: $('#password').val()
},
type: 'POST',
dataType: 'json'
});
}
if (checkPassword()) {
// Tell the user they're logged in
}
Цей код неправильно розуміє вищезазначені проблеми асинхронності. Зокрема, $.ajax()
він не заморожує код під час перевірки сторінки "/ пароль" на вашому сервері - він надсилає запит на сервер і, поки він чекає, він негайно повертає jQuery Ajax Deferred об'єкт, а не відповідь від сервера. Це означає, що if
оператор завжди отримуватиме цей відкладений об’єкт, трактуватиме його як true
і продовжувати так, ніби користувач увійшов у систему. Не добре.
Але виправити це легко:
checkPassword()
.done(function(r) {
if (r) {
// Tell the user they're logged in
} else {
// Tell the user their password was bad
}
})
.fail(function(x) {
// Tell the user something bad happened
});
Не рекомендується: синхронні дзвінки "Ajax"
Як я вже згадував, деякі (!) Асинхронні операції мають синхронні аналоги. Я не виступаю за їх використання, але заради повноти, ось як би ви виконували синхронний дзвінок:
Без jQuery
Якщо ви безпосередньо використовуєте XMLHTTPRequest
об'єкт, передайте його false
як третій аргумент .open
.
jQuery
Якщо ви використовуєте jQuery , ви можете встановити async
параметр на false
. Зауважте, що ця опція застаріла з часу jQuery 1.8. Тоді ви все одно можете використовувати success
зворотний виклик або отримати доступ до responseText
властивості об'єкта jqXHR :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Якщо ви використовуєте будь-який інший метод jQuery Ajax, такий як $.get
, $.getJSON
тощо, вам доведеться змінити його $.ajax
(оскільки ви можете передавати лише параметри конфігурації $.ajax
).
Голова вгору! Неможливо зробити синхронний запит JSONP . JSONP за своєю суттю завжди асинхронний (ще одна причина навіть не розглянути цей варіант).