→ Для более общего объяснения асинхронного поведения с различными примерами см. Почему моя переменная не изменяется после ее изменения внутри функции? - Справочник по асинхронному коду
→ Если вы уже понимаете проблему, перейдите к возможным решениям ниже.
Эта проблема
A в 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 the 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
и _ 21_ в MDN.
Вот пример, описывающий функцию delay findItem()
выше:
// 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);
})();
Текущий браузер и node версии поддерживают async/await
. Вы также можете поддерживать старые среды, преобразовав свой код в ES5 с помощью регенератора (или инструментов, использующих регенератор , например Babel).
Разрешить функциям принимать обратные вызовы
Обратный вызов - это когда функция 1 передается функции 2. Функция 2 может вызывать функцию 1, когда она готова. В контексте асинхронного процесса обратный вызов будет вызываться всякий раз, когда асинхронный процесс завершается. Обычно результат передается обратному вызову.
В примере вопроса вы можете заставить foo
принять обратный вызов и использовать его как success
обратный вызов. Так что это
var result = foo();
// Code that depends on 'result'
становится
foo(function(result) {
// Code that depends on 'result'
});
Здесь мы определили встроенную функцию, но вы можете передать любую ссылку на функцию:
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 и предоставляют дополнительные методы для упрощения использования и композиции асинхронных функций (например, синяя птица).
Обещания - это контейнеры для будущих значений. Когда обещание получает значение (оно разрешено) или когда оно отменяется (отклонено), оно уведомляет всех своих слушателей, которые хотят получить доступ к этому значению.
Преимущество перед обычными обратными вызовами заключается в том, что они позволяют отделить ваш код и их легче составлять.
Вот пример использования обещания:
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).
});
.as-console-wrapper { max-height: 100% !important; top: 0; }
Применительно к нашему вызову 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("https://jsonplaceholder.typicode.com/todos/1")
.then(function(result) {
console.log(result); // Code depending on result
})
.catch(function() {
// An error occurred
});
.as-console-wrapper { max-height: 100% !important; top: 0; }
Описание всех преимуществ, которые предлагают обещания, выходит за рамки этого ответа, но если вы пишете новый код, вам следует серьезно их рассмотреть. Они обеспечивают отличную абстракцию и разделение вашего кода.
Дополнительная информация о обещаниях: Скалы HTML5 - обещания JavaScript.
Боковое примечание: отложенные объекты jQuery
Отложенные объекты - это пользовательская реализация обещаний jQuery (до стандартизации Promise API). Они ведут себя почти как обещания, но предоставляют немного другой 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()
не замораживает код при проверке страницы '/ password' на вашем сервере - он отправляет запрос на сервер и пока ожидает, он немедленно возвращает объект 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
в качестве третьего аргумента в _ 51_.
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 по своей природе всегда асинхронен (еще одна причина даже не рассматривать этот вариант).
person
Felix Kling
schedule
08.01.2013