→ Для более общего объяснения асинхронного поведения на разных примерах см. Почему моя переменная не изменяется после того, как я изменил ее внутри функции? - асинхронная ссылка на код
→ Если вы уже поняли проблему, перейдите к возможным решениям ниже.
Проблема
В 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. Чтобы в конечном итоге получить ответ, вы предоставляете функцию, которая будет вызываться после получения ответа, обратный вызов (заметьте что-нибудь, « перезвоните» ). Любой оператор, следующий за этим вызовом, выполняется до вызова обратного вызова.
Решение (s)
Примите асинхронную природу 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). Получение ответа Ajax - не что иное, как событие.
Сложности могут возникнуть, когда вам придется работать со сторонним кодом, но большинство проблем можно решить, просто продумав поток приложения.
ES2015 +: обещания с then ()
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.
Каждый Ajax-метод jQuery уже возвращает «отложенный объект» (фактически обещание отложенного объекта), который вы можете просто вернуть из своей функции:
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 по своей природе всегда асинхронен (еще одна причина, чтобы даже не рассматривать эту опцию).