Имам функция foo
, която прави заявка от Ajax. Как мога да върна отговора от foo
?
Опитах се да върна стойността от обратната връзка success
, както и да присвоя отговора на локална променлива вътре във функцията и да върна тази променлива, но нито един от тези начини не връща отговора.
function foo() {
var result;
$.ajax({
url: '...',
success: function(response) {
result = response;
// return response; // <- I tried that one as well
}
});
return result;
}
var result = foo(); // It always ends up being `undefined`.
→ За по-общо обяснение на асинхронното поведение с различни примери, моля, вижте https://stackoverflow.com/q/23667086/218196
→ Ако вече сте разбрали проблема, преминете към възможните решения по-долу.
Проблемът
А в 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();
Приемайте асинхронната природа на JavaScript! Въпреки че някои асинхронни операции предоставят синхронни аналози (така е и при "Ajax"), като цяло не се препоръчва да се използват, особено в контекста на браузъра. Питате се защо това е лошо? JavaScript се изпълнява в нишката на потребителския интерфейс на браузъра и всеки дълъг процес ще блокира потребителския интерфейс, което ще го направи нереагиращ. Освен това има горна граница на времето за изпълнение на JavaScript и браузърът ще попита потребителя дали да продължи изпълнението или не. Всичко това е наистина лошо за потребителя. Потребителят няма да може да разбере дали всичко работи добре или не. Освен това ефектът ще бъде по-лош за потребителите с бавна връзка. По-нататък ще разгледаме три различни решения, които се надграждат едно над друго:
async/await
(ES2017+, достъпно в по-стари браузъри, ако използвате транспортер или регенератор)then()
(ES2015+, достъпни в по-стари браузъри, ако използвате някоя от многото библиотеки за обещания)
И трите са налични в настоящите браузъри и в node 7+.async/await
Версията на ECMAScript, издадена през 2017 г., въвежда поддръжка на ниво синтаксис за асинхронни функции. С помощта на async
и await
можете да пишете асинхронни функции в "синхронен стил". Кодът все още е асинхронен, но'е по-лесен за четене/разбиране.
async/await
се основава на обещания: функция async
винаги връща обещание. await
"разопакова" обещание и в резултат получава стойността, с която е било резолирано обещанието, или хвърля грешка, ако обещанието е било отхвърлено.
Важно: Можете да използвате await
само в рамките на функция async
. В момента все още не се поддържа await
на най-високо ниво, така че може да се наложи да направите асинхронен IIFE (Immediately Invoked Function Expression), за да стартирате контекст на 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 с помощта на regenerator (или инструменти, които използват regenerator, като 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);
}
});
}
Приложенията Promise API са нова функция на ECMAScript 6 (ES2015), но вече имат добра браузърна поддръжка. Съществуват и много библиотеки, които имплементират стандартния Promises API и предоставят допълнителни методи за улесняване на използването и съставянето на асинхронни функции (например bluebird). 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).
});
Приложено към нашето 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 rocks - JavaScript Promises
Deferred objects са собствена реализация на обещанията в jQuery (преди 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
винаги ще получава този Deferred обект, ще го третира като 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
});
Както споменах, някои(!) асинхронни операции имат синхронни аналози. Не препоръчвам използването им, но за пълнота на изложението, ето как бихте извършили синхронно извикване:
Ако използвате директно обект XMLHTTPRequest
, подайте false
като трети аргумент на .open
.
Ако използвате 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 винаги е асинхронна (още една причина дори да не обмисляте тази опция).
Кодът ви трябва да е нещо подобно на това:
function foo() {
var httpRequest = new XMLHttpRequest();
httpRequest.open('GET', "/echo/json");
httpRequest.send();
return httpRequest.responseText;
}
var result = foo(); // always ends up being 'undefined'
Феликс Клинг свърши добра работа, като написа отговор за хората, които използват jQuery за AJAX, а аз реших да предоставя алтернатива за хората, които не го правят.
Това е кратко резюме на "Обяснение на проблема" от другия отговор, ако'не сте сигурни след като прочетете това, прочетете него.
А в AJAX означава асинхронно. Това означава, че изпращането на заявката (или по-скоро получаването на отговора) е извадено от нормалния поток на изпълнение. Във вашия пример .send
се връща веднага и следващото изречение, return result;
, се изпълнява преди функцията, която сте предали като обратна връзка success
, дори да е била извикана.
Това означава, че когато се връщате, слушателят, който сте дефинирали, все още не се е изпълнил, което означава, че стойността, която връщате, не е била дефинирана.
Ето една проста аналогия
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
Върнатата стойност на a
е неопределена
, тъй като частта a=5
все още не се е изпълнила. AJAX действа по този начин, вие връщате стойност, преди сървърът да е имал възможност да каже на браузъра каква е тази стойност.
Едно възможно решение на този проблем е да кодирате реактивно , като кажете на програмата си какво да направи, когато изчислението приключи.
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
Това се нарича CPS. В общи линии ние'подаваме на getFive
действие, което да извърши, когато то завърши, казваме на нашия код как да реагира, когато завърши дадено събитие (например нашето AJAX повикване или в този случай таймаут).
Използването би било следното:
getFive(onComplete);
Което трябва да предупреди "5" на екрана. [(Fiddle)][4].
Има основно два начина за решаване на този проблем:
Що се отнася до синхронния AJAX, не го правете! В отговора на Felix се изтъкват някои убедителни аргументи защо това е лоша идея. Ако трябва да обобщим, това ще замрази браузъра на потребителя, докато сървърът върне отговора, и ще създаде много лошо потребителско преживяване. Ето още едно кратко обобщение, взето от MDN, за причините:
XMLHttpRequest поддържа както синхронни, така и асинхронни комуникации. Като цяло обаче асинхронните заявки трябва да се предпочитат пред синхронните заявки по причини, свързани с производителността.
Накратко, синхронните заявки блокират изпълнението на кода... ...това може да доведе до сериозни проблеми...
Ако ви се налага да го направите, можете да подадете флаг: Ето как:
var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false); // `false` makes the request synchronous
request.send(null);
if (request.status === 200) {// That's HTTP for 'ok'
console.log(request.responseText);
}
Позволете на вашата функция да приема обратна връзка. В кода на примера foo
може да се направи така, че да приема обратна връзка. Ще кажем на нашия код как да реагира, когато foo
завърши.
И така:
var result = foo();
// code that depends on `result` goes here
Става:
foo(function(result) {
// code that depends on `result`
});
Тук предадохме анонимна функция, но също толкова лесно бихме могли да предадем препратка към съществуваща функция, така че да изглежда така:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
За повече подробности относно начина на проектиране на този вид обратни повиквания вижте отговора на Felix'.
Сега нека дефинираме самия foo да действа по съответния начин
function foo(callback) {
var httpRequest = new XMLHttpRequest();
httpRequest.onload = function(){ // when the request is loaded
callback(httpRequest.responseText);// we're calling our method
};
httpRequest.open('GET', "/echo/json");
httpRequest.send();
}
[(fiddle)][6]
Сега направихме така, че функцията ни foo да приема действие, което да се изпълнява при успешно завършване на AJAX-а. Можем да разширим това още повече, като проверим дали статусът на отговора не е 200 и да действаме по съответния начин (да създадем обработващ fail и други подобни). Ефективно решаване на нашия проблем.
Ако все още се затруднявате да разберете това прочетете ръководството за започване на работа с AJAX в MDN.
XMLHttpRequest 2 (първо прочетете отговорите от Benjamin Gruenbaum & Felix Kling) Ако не използвате jQuery и искате хубав кратък XMLHttpRequest 2, който да работи на съвременните браузъри, а също и на мобилните браузъри, предлагам да го използвате по този начин:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
Както можете да видите:
this.response
Или ако по някаква причина свържете()
обратното повикване с клас:
e.target.response
Пример:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Или (горният вариант е по-добър анонимните функции винаги са проблем):
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
е друга голяма грешка, тъй като трябва да изпълните обратното извикване вътре в затворите на onload/oreadystatechange, иначе го губите.Сега, ако искате нещо по-сложно, използвайки post и FormData, можете лесно да разширите тази функция:
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.send(d||null)
}
Отново ... това е много кратка функция, но тя получава & post. Примери за използване:
x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data
Или подайте пълен елемент от формата (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
Или задайте някои потребителски стойности:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Както беше споменато в коментара, използването на error && synchronous напълно нарушава смисъла на отговора. Кой е хубавият кратък начин да използвате Ajax по правилния начин? Обработчик на грешки
function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
c = new XMLHttpRequest;
c.open(e||'get', a);
c.onload = b;
c.onerror = error;
c.send(d||null)
}
function error(e){
console.log('--Error--', this.type);
console.log('this: ', this);
console.log('Event: ', e)
}
function displayAjax(e){
console.log(e, this);
}
x('WRONGURL', displayAjax);
displayAjax()
под this.statusText
като Method not Allowed
.
Във втория случай тя просто работи. Трябва да проверите от страна на сървъра дали сте предали правилните данни за публикацията.
Cross-domain not allowed хвърля грешка автоматично.
В отговора за грешка няма кодове за грешки.
Има само this.type
, който е зададен на грешка.
Защо да добавяте програма за обработка на грешки, ако нямате пълен контрол върху грешките?
Повечето от грешките се връщат вътре в тази функция за обратно извикване displayAjax()
.
И така: Няма нужда от проверка за грешки, ако сте в състояние да копирате и поставите правилно URL адреса. ;)
PS: Като първи тест написах x('x', displayAjax)..., и напълно получих отговор...???? Затова проверих папката, в която се намира HTML, и там имаше файл, наречен 'x.xml'. Така че дори да забравите разширението на вашия файл, XMLHttpRequest 2 ЩЕ ГО НАМЕРИ*. LOL'dСинхронно четене на файл
Не правете това.
Ако искате да блокирате браузъра за известно време, заредете хубав голям .txt
файл синхронно.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Сега можете да направите
var res = omg('thisIsGonnaBlockThePage.txt');
Горните функции са за основна употреба. Ако искате да РАЗШИРИТЕ функцията... Да, можете. Аз'използвам много API и една от първите функции, които интегрирам във всяка HTML страница, е първата Ajax функция в този отговор, само с GET... Но можете да правите много неща с XMLHttpRequest 2: Направих мениджър за изтегляне (използвайки диапазони от двете страни с възобновяване, filereader, файлова система), различни конвертори за промяна на размера на изображението, използвайки платно, попълване на уеб SQL бази данни с base64images и много други... Но в тези случаи трябва да създадете функция само за тази цел... понякога се нуждаете от blob, масивни буфери, можете да задавате хедъри, да пренаписвате mimetype и има още много други неща... Но въпросът тук е как да върнете Ajax отговор... (Добавих един лесен начин.)