→ Для более общего объяснения асинхронного поведения с различными примерами см. Почему моя переменная не изменяется после того, как я изменяю ее внутри функции? - Ссылка на асинхронный код
→ Если вы уже понимаете проблему, перейдите к возможным решениям ниже.
Проблема
A в Ajax означает асинхронный <710519>. Это означает, что отправка запроса (или, скорее, получение ответа) исключается из обычного потока выполнения. В вашем примере $.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 (Immediately Invoked Function Expression), чтобы запустить контекст async
.
Подробнее о async
и await
можно узнать в MDN.
Вот пример, в котором подробно описывается функция задержки , , 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);
})();
Текущая версия браузера и узел поддерживают версии 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'
});
Здесь мы определили функцию "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 - не что иное, как событие.
При работе со сторонним кодом могут возникнуть трудности, но большинство проблем можно решить, просто продумав поток приложения.
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).
});
.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 Rock - JavaScript Promises.
Примечание: отложенные объекты jQuery
Отложенные объекты - это пользовательская реализация обещаний jQuery (до стандартизации Promise API). Они ведут себя почти как обещания, но предоставляют немного другой API.
Каждый Ajax-метод jQuery уже возвращает «отложенный объект» (фактически обещание отложенного объекта), который вы можете просто вернуть из своей функции:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Боковое примечание: подводные камни Promise
Имейте в виду, что обещания и отложенные объекты - это просто контейнеры для будущего значения, а не само значение. Например, предположим, что у вас есть следующее:
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
в качестве третьего аргумента для .open
<710.
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 по самой своей природе всегда асинхронен (еще одна причина даже не рассматривать этот вариант).
используйте деасинхронизацию, как это coderhelper.com/a/47051880/2083877
@SunilKumar Я не думаю, что это полезно. OP задал этот вопрос и сам ответил, чтобы задокументировать, как получить ответ от асинхронных вызовов. Предложение стороннего модуля побеждает эту цель, и ИМО парадигма, представленная этим модулем, не является хорошей практикой.
Не пора ли избавиться от jQuery в этом вопросе? Достаточно ли это наследие в 2021 году?
@Liam: это просто пример асинхронной функции, которая принимает обратный вызов.
Имеет смысл, я изменил заголовок, чтобы не выделять jQuery.