Mám funkciu foo
, ktorá vykoná požiadavku Ajax. Ako môžem vrátiť odpoveď z funkcie foo
?
Skúsil som vrátiť hodnotu zo spätného volania success
, ako aj priradiť odpoveď do lokálnej premennej vo vnútri funkcie a vrátiť ju, ale ani jeden z týchto spôsobov nevrátil odpoveď.
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`.
→ Všeobecnejšie vysvetlenie asynchrónneho správania s rôznymi príkladmi nájdete na https://stackoverflow.com/q/23667086/218196
→ Ak už problému rozumiete, prejdite na možné riešenia nižšie.
Problém
Skratka A v slove Ajax znamená asynchrónny . To znamená, že odoslanie požiadavky (alebo skôr prijatie odpovede) je vyňaté z normálneho toku vykonávania. Vo vašom príklade sa
$.ajax
vráti okamžite a ďalší príkaz,return result;
, sa vykoná ešte pred tým, ako bola zavolaná funkcia, ktorú ste odovzdali ako spätné volaniesuccess
. Tu je analógia, ktorá snáď objasní rozdiel medzi synchrónnym a asynchrónnym tokom:Synchrónny
Predstavte si, že telefonujete priateľovi a požiadate ho, aby vám niečo vyhľadal. Hoci to môže chvíľu trvať, čakáte na telefóne a hľadíte do prázdna, kým vám priateľ nedá odpoveď, ktorú ste potrebovali. To isté sa deje, keď voláte funkciu obsahujúcu "normálny" kód:
function findItem() {
var item;
while(item_not_found) {
// search
}
return item;
}
var item = findItem();
// Do something with item
doSomethingElse();
Aj keď vykonanie funkcie findItem
môže trvať dlho, každý kód, ktorý príde za var item = findItem();
, musí čakať, kým funkcia vráti výsledok.
Z toho istého dôvodu zavoláte svojho priateľa znova. Tentoraz mu však poviete, že sa ponáhľate a mal by vám zavolať späť na mobilný telefón. Zavesíte, odídete z domu a urobíte to, čo ste mali v pláne. Keď vám váš priateľ zavolá späť, zaoberáte sa informáciami, ktoré vám poskytol. Presne to'sa deje, keď vykonáte požiadavku Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
Prijmite asynchrónnu povahu JavaScriptu! Hoci niektoré asynchrónne operácie poskytujú synchrónne náprotivky (tak je to aj v prípade "Ajax"), vo všeobecnosti sa neodporúča ich používať, najmä v kontexte prehliadača. Pýtate sa, prečo je to zlé? JavaScript beží vo vlákne používateľského rozhrania prehliadača a akýkoľvek dlho bežiaci proces zablokuje používateľské rozhranie, takže nebude reagovať. Okrem toho existuje horný limit času vykonávania JavaScriptu a prehliadač sa používateľa opýta, či má pokračovať vo vykonávaní alebo nie. To všetko je pre používateľa naozaj nepríjemné. Používateľ nebude schopný zistiť, či všetko funguje v poriadku alebo nie. Okrem toho bude tento efekt horší pre používateľov s pomalým pripojením. V nasledujúcom texte sa pozrieme na tri rôzne riešenia, ktoré na seba nadväzujú:
async/await
(ES2017+, dostupné v starších prehliadačoch, ak používate transpiler alebo regenerátor)then()
(ES2015+, dostupné v starších prehliadačoch, ak používate jednu z mnohých knižníc sľubov)
Všetky tri sú k dispozícii v súčasných prehliadačoch a v uzle 7+.async/await
Verzia jazyka ECMAScript vydaná v roku 2017 zaviedla podporu asynchrónnych funkcií na úrovni syntaxe. Pomocou async
a await
môžete písať asynchrónne v "synchrónnom štýle". Kód je stále asynchrónny, ale je'ľahšie čitateľný/pochopiteľný.
async/await
stavia na prísľuboch: funkcia async
vždy vracia prísľub. await
"rozbalí" prísľub a buď výsledkom je hodnota, s ktorou bol prísľub vyriešený, alebo vyhodí chybu, ak bol prísľub odmietnutý.
Dôležité: Funkciu await
môžete použiť len vo vnútri funkcie async
. Momentálne ešte nie je podporovaná najvyššia úroveň await
, takže možno budete musieť vytvoriť asynchrónny IIFE (Immediately Invoked Function Expression) na spustenie kontextu async
.
Viac informácií o async
a await
nájdete na MDN.
Tu je príklad, ktorý nadväzuje na vyššie uvedené oneskorenie:
// 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
. Staršie prostredia môžete podporovať aj transformáciou kódu na ES5 pomocou regenerator (alebo nástrojov, ktoré používajú regenerator, ako napríklad Babel).Spätné volanie je jednoducho funkcia odovzdaná inej funkcii. Táto iná funkcia môže zavolať odovzdanú funkciu vždy, keď je pripravená. V kontexte asynchrónneho procesu sa spätné volanie zavolá vždy, keď je asynchrónny proces hotový. Zvyčajne sa spätnému volaniu odovzdáva výsledok.
V príklade z otázky môžete urobiť tak, že foo
prijme spätné volanie a použije ho ako spätné volanie success
. Takže toto
var result = foo();
// Code that depends on 'result'
sa stane
foo(function(result) {
// Code that depends on 'result'
});
Tu sme definovali funkciu "inline", ale môžete odovzdať akýkoľvek odkaz na funkciu:
function myCallback(result) {
// Code that depends on 'result'
}
foo(myCallback);
Samotné foo
je definované takto:
function foo(callback) {
$.ajax({
// ...
success: callback
});
}
callback
bude odkazovať na funkciu, ktorú odovzdáme foo
pri jej volaní a jednoducho ju odovzdáme success
. T.j. keď je požiadavka Ajaxu úspešná, $.ajax
zavolá callback
a odovzdá odpoveď spätnému volaniu (na ktoré sa môžeme odvolať pomocou result
, keďže takto sme definovali spätné volanie).
Odpoveď môžete spracovať aj pred jej odovzdaním spätnému volaniu:
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
Rozhranie Promise API je novou funkciou jazyka ECMAScript 6 (ES2015), ale má už dobrú podporu prehliadačov. Existuje aj mnoho knižníc, ktoré implementujú štandardné API Promises a poskytujú ďalšie metódy na uľahčenie používania a skladania asynchrónnych funkcií (napr. bluebird). Prísľuby sú kontajnery pre budúce hodnoty. Keď sľub prijme hodnotu (je vyriešený) alebo keď je zrušený (odmietnutý), oznámi to všetkým svojim "poslucháčom", ktorí chcú k tejto hodnote pristupovať. Výhodou oproti obyčajným spätným volaniam je, že umožňujú oddeliť kód a ľahšie sa skladajú. Tu je jednoduchý príklad použitia sľubu:
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).
});
Pri použití na naše volanie Ajaxu by sme mohli použiť sľuby takto:
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
});
Opis všetkých výhod, ktoré sľuby ponúkajú, je nad rámec tejto odpovede, ale ak píšete nový kód, mali by ste o nich vážne uvažovať. Poskytujú skvelú abstrakciu a oddelenie vášho kódu. Ďalšie informácie o sľuboch: HTML5 rocks - JavaScript Promises.
Deferred objects sú vlastnou implementáciou sľubov v jQuery (pred štandardizáciou API Promise). Správajú sa takmer ako sľuby, ale poskytujú trochu iné API. Každá Ajaxová metóda jQuery už vracia "deferred object" (v skutočnosti prísľub deferred objektu), ktorý môžete jednoducho vrátiť z vašej funkcie:
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Nezabudnite, že sľuby a odložené objekty sú len kontajnery pre budúcu hodnotu, nie sú samotnou hodnotou. Predpokladajme napríklad, že máte nasledovné:
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
}
Tento kód nesprávne chápe vyššie uvedené problémy asynchrónie. Konkrétne, $.ajax()
nezastaví kód, kým kontroluje stránku '/heslo' na vašom serveri - odošle požiadavku na server a kým čaká, okamžite vráti objekt jQuery Ajax Deferred, nie odpoveď zo servera. To znamená, že príkaz if
vždy získa tento objekt Deferred, bude ho považovať za true
a bude pokračovať, ako keby bol používateľ prihlásený. To nie je dobré.
Oprava je však jednoduchá:
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
});
Ako som už spomenul, niektoré(!) asynchrónne operácie majú synchrónne ekvivalenty. Neobhajujem ich používanie, ale pre úplnosť uvádzam, ako by ste vykonali synchrónne volanie:
Ak priamo použijete objekt XMLHTTPRequest
, odovzdajte ako tretí argument príkazu .open
príkaz false
.
Ak používate jQuery, môžete nastaviť možnosť async
na hodnotu false
. Všimnite si, že táto možnosť je zrušená od verzie jQuery 1.8.
Potom môžete stále používať spätné volanie success
alebo pristupovať k vlastnosti responseText
objektu jqXHR:
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Ak používate akúkoľvek inú metódu jQuery Ajax, napríklad $.get
, $.getJSON
atď., musíte ju zmeniť na $.ajax
(pretože $.ajax
môžete odovzdať iba konfiguračné parametre).
Upozornenie! Nie je možné vykonať synchrónnu požiadavku JSONP. JSONP je zo svojej podstaty vždy asynchrónny (ďalší dôvod, prečo o tejto možnosti ani neuvažovať).
Váš kód by mal vyzerať približne takto:
function foo() {
var httpRequest = new XMLHttpRequest();
httpRequest.open('GET', "/echo/json");
httpRequest.send();
return httpRequest.responseText;
}
var result = foo(); // always ends up being 'undefined'
Felix Kling odviedol dobrú prácu pri písaní odpovede pre ľudí, ktorí používajú jQuery pre AJAX, ja'som sa rozhodol poskytnúť alternatívu pre ľudí, ktorí ju nepoužívajú.
Toto je krátke zhrnutie "Vysvetlenia problému" z inej odpovede, ak si po prečítaní tohto nie ste istí, prečítajte si to.
Skratka A v slove AJAX znamená asynchrónny. To znamená, že odoslanie požiadavky (alebo skôr prijatie odpovede) je vyňaté z normálneho toku vykonávania. Vo vašom príklade sa príkaz .send
vráti okamžite a ďalší príkaz, return result;
, sa vykoná ešte pred tým, ako bola zavolaná funkcia, ktorú ste odovzdali ako spätné volanie success
.
To znamená, že keď'sa vracia, poslucháč, ktorý ste'definovali, sa ešte nevykonal, čo znamená, že hodnota, ktorú vraciate, ešte nebola definovaná.
Tu je jednoduchá analógia
function getFive(){
var a;
setTimeout(function(){
a=5;
},10);
return a;
}
[(Fiddle)][2]
Vrátená hodnota a
je nedefinovaná
, pretože časť a=5
sa ešte nevykonala. AJAX sa správa tak, že vraciate hodnotu skôr, ako server dostal šancu povedať prehliadaču, aká je táto hodnota.
Jedným z možných riešení tohto problému je kódovať reaktívne , čím svojmu programu poviete, čo má urobiť po dokončení výpočtu.
function onComplete(a){ // When the code completes, do this
alert(a);
}
function getFive(whenDone){
var a;
setTimeout(function(){
a=5;
whenDone(a);
},10);
}
Tento postup sa nazýva CPS. V podstate'odovzdávame getFive
akciu, ktorú má vykonať po dokončení, hovoríme nášmu kódu, ako má reagovať po dokončení udalosti (ako je naše volanie AJAXu alebo v tomto prípade časový limit).
Použitie by bolo nasledovné:
getFive(onComplete);
Čo by malo upozorniť "5" na obrazovke. [(Fiddle)][4].
V zásade existujú dva spôsoby, ako to vyriešiť:
Čo sa týka synchrónneho AJAXu, nepoužívajte ho! Felix vo svojej odpovedi uvádza niekoľko presvedčivých argumentov, prečo je to zlý nápad. Ak to zhrnieme, zamrzne používateľovi prehliadač, kým server nevráti odpoveď, a vytvorí veľmi zlú používateľskú skúsenosť. Tu je ďalšie krátke zhrnutie z MDN o tom, prečo:
XMLHttpRequest podporuje synchrónnu aj asynchrónnu komunikáciu. Vo všeobecnosti by sa však z výkonnostných dôvodov mali uprednostňovať asynchrónne požiadavky pred synchrónnymi.
Stručne povedané, synchrónne požiadavky blokujú vykonávanie kódu... ...to môže spôsobiť vážne problémy...
Ak to musíte urobiť, môžete odovzdať príznak: Tu je návod ako:
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);
}
Nechajte svoju funkciu prijať spätné volanie. V príklade kódu foo
môže byť funkcia prijatá ako spätné volanie. Budeme nášmu kódu hovoriť, ako má reagovať, keď sa foo
dokončí.
Takže:
var result = foo();
// code that depends on `result` goes here
sa stáva:
foo(function(result) {
// code that depends on `result`
});
Tu sme odovzdali anonymnú funkciu, ale rovnako ľahko by sme mohli odovzdať odkaz na existujúcu funkciu, takže by to vyzeralo takto:
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
Podrobnejšie informácie o tom, ako sa tento druh spätného volania navrhuje, nájdete v odpovedi Felixa.
Teraz definujme samotné foo, aby sa správalo zodpovedajúcim spôsobom
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]
Teraz sme vytvorili našu funkciu foo, ktorá akceptuje akciu, ktorá sa spustí po úspešnom dokončení AJAXu, môžeme ju ďalej rozšíriť o kontrolu, či stav odpovede nie je 200, a podľa toho konať (vytvoriť obslužný program fail a podobne). Efektívne tak vyriešite náš problém.
Ak'to stále nechápete prečítajte si príručku AJAX getting started guide na MDN.
XMLHttpRequest 2 (najprv si prečítajte odpovede od Benjamin Gruenbaum & Felix Kling) Ak nepoužívate jQuery a chcete pekný krátky XMLHttpRequest 2, ktorý funguje v moderných prehliadačoch a tiež v mobilných prehliadačoch, odporúčam ho použiť týmto spôsobom:
function ajax(a, b, c){ // URL, callback, just a placeholder
c = new XMLHttpRequest;
c.open('GET', a);
c.onload = b;
c.send()
}
Ako vidíte:
this.response
Alebo ak z nejakého dôvodu priviažete()
spätné volanie na triedu:
e.target.response
Príklad:
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Alebo (vyššie uvedený je lepší anonymné funkcie sú vždy problém):
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
je ďalšia veľká chyba, pretože musíte vykonať spätné volanie vnútri uzáverov onload/oreadystatechange, inak ste oň prišli.Ak teraz chcete niečo zložitejšie pomocou post a FormData, môžete túto funkciu jednoducho rozšíriť:
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)
}
Opäť ... je to'veľmi krátka funkcia, ale dostane & post. Príklady použitia:
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
Alebo odovzdajte celý prvok formulára (document.getElementsByTagName('form')[0]
):
var fd = new FormData(form);
x(url, callback, 'post', fd);
Alebo nastavte nejaké vlastné hodnoty:
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Ako je uvedené v komentári, použitie chyby && synchronous úplne rozbíja zmysel odpovede. Ktorý je pekný krátky spôsob použitia Ajaxu správnym spôsobom? Obsluha chyby
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()
pod this.statusText
ako Method not Allowed
.
V druhom prípade to jednoducho funguje. Musíte skontrolovať na strane servera, či ste odovzdali správne údaje o príspevku.
Cross-domain not allowed automaticky vyhodí chybu.
V chybovej odpovedi nie sú uvedené žiadne chybové kódy.
Je tam len this.type
, ktorý je nastavený na error.
Načo pridávať obsluhu chýb, keď nad chybami nemáte absolútne žiadnu kontrolu?
Väčšina chýb sa vracia vo vnútri tohto vo funkcii spätného volania displayAjax()
.
Takže: Ak ste schopní správne skopírovať a vložiť URL, nie je potrebné kontrolovať chyby.)
PS: Ako prvý test som napísal x('x', displayAjax)..., a úplne to dostalo odpoveď...??? Tak som skontroloval priečinok, kde sa nachádza HTML, a tam bol súbor s názvom 'x.xml'. Takže aj keď zabudnete na príponu vášho súboru XMLHttpRequest 2 ho nájde. LOL'd somSynchrónne čítanie súboru
Nedávajte to dokopy.
Ak chcete na chvíľu zablokovať prehliadač, načítajte pekný veľký súbor .txt
synchrónne.
function omg(a, c){ // URL
c = new XMLHttpRequest;
c.open('GET', a, true);
c.send();
return c; // Or c.response
}
Teraz môžete urobiť
var res = omg('thisIsGonnaBlockThePage.txt');
Uvedené funkcie sú určené na základné použitie. Ak chcete funkciu ROZŠÍRIŤ... Áno, môžete. Ja'používam veľa API a jednou z prvých funkcií, ktoré integrujem do každej stránky HTML, je prvá funkcia Ajax v tejto odpovedi, pričom GET je len... Ale s XMLHttpRequest 2 môžete robiť veľa vecí: Urobil som správcu sťahovania (pomocou rozsahov na oboch stranách s obnovením, filereader, súborový systém), rôzne konvertory veľkosti obrázkov pomocou plátna, naplniť webové databázy SQL s base64images a oveľa viac... Ale v týchto prípadoch by ste mali vytvoriť funkciu len na tento účel... niekedy potrebujete blob, pole bufferov, môžete nastaviť hlavičky, prepísať mimetype a je toho oveľa viac... Ale tu ide o to, ako vrátiť odpoveď Ajaxu... (Pridal som jednoduchý spôsob.)