Kako vratiti odgovor iz asinkronog poziva?

Imam foo funkciju koja čini Ajax zahtjev. Kako vratiti odgovor iz foo ?

Pokušao sam vratiti vrijednost iz povratnog poziva za success i također dodijeliti odgovor lokalnoj varijabli unutar funkcije i vratiti je, ali niti jedna od tih metoda ne vraća odgovor.

 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`. 
4687
08 янв. Felix Kling postavljen 8. siječnja 2013-01-08 20:06 '13 u 20:06 2013-01-08 20:06
@ 39 odgovora
  • 1
  • 2

→ Za općenitije objašnjenje asinkronog ponašanja u različitim primjerima pogledajte: Zašto se moja varijabla ne mijenja nakon što je promijenim unutar funkcije? - asinkroni link na kod

→ Ako ste već shvatili problem, prijeđite na moguća rješenja u nastavku.

Ovaj problem

A u Ajaxu znači asinkrono . To znači da je slanje zahtjeva (ili, prije, primanje odgovora) uklonjeno iz normalnog tijeka izvršenja. U vašem primjeru, $.ajax se odmah vraća, a sljedeći izraz return result; , izvršava se prije nego što je čak i pozvana funkcija koju ste poslali kao povratni poziv za success .

Ovo je analogija koja, nadamo se, pojašnjava razliku između sinkronog i asinkronog toka:

sinkroni

Zamislite da zovete prijatelja i tražite od njega da pronađe nešto za vas. Iako može potrajati neko vrijeme, pričekajte na telefonu i potražite u svemiru sve dok vam prijatelj ne da odgovor koji vam je potreban.

Ista stvar se događa kada napravite poziv funkcije koji sadrži "normalan" kod:

 function findItem() { var item; while(item_not_found) { // search } return item; } var item = findItem(); // Do something with item doSomethingElse(); 

Čak i ako može potrajati dugo da se izvrši findItem , bilo koji kôd koji slijedi var item = findItem(); mora čekati dok funkcija ne vrati rezultat.

asinhron

Zovi prijatelja ponovno iz istog razloga. Ali ovaj put mu reci da se žuriš, i trebao bi te nazvati na tvoj mobilni telefon. Spustite slušalicu, napustite kuću i učinite sve što ste planirali. Čim vas vaš prijatelj nazove, razmislit ćete o informacijama koje vam je dao.

To je upravo ono što se događa kada napravite Ajax zahtjev.

 findItem(function(item) { // Do something with item }); doSomethingElse(); 

Umjesto čekanja na odgovor, izvršenje se nastavlja odmah i izraz se izvršava nakon Ajax poziva. Da biste naposljetku dobili odgovor, dajete funkciju koja će biti pozvana nakon primitka odgovora, povratnog poziva (zabilježite nešto, "nazovite"). Bilo koja izjava nakon ovog poziva izvršava se prije poziva za povratni poziv.


Rješenje (a)

Usvojite asinkronu prirodu javascripta! Iako neke asinkrone operacije pružaju sinkrone kopije (kao što je "Ajax"), obično se ne preporučuju za korištenje, osobito u kontekstu preglednika.

Zašto je to loše, pitate?

JavaScript se pokreće u thread-u korisničkog sučelja preglednika, a bilo koji dugotrajan proces blokira korisničko sučelje, što ga čini neodzivnim. Osim toga, postoji i gornje ograničenje vremena izvođenja za JavaScript, a preglednik će od korisnika zatražiti da nastavi s izvođenjem ili ne.

Sve je to stvarno loše korisničko iskustvo. Korisnik neće moći reći je li sve ispravno ili ne. Osim toga, učinak će biti lošiji za korisnike s sporom vezom.

Zatim ćemo pogledati tri različita rješenja koja su izgrađena jedan na drugom:

  • Obećanja s async/await await (ES2017 +, dostupna u starijim preglednicima ako koristite transporter ili regenerator)
  • Povratni pozivi (popularno na web-lokaciji)
  • Obećanja s then() (ES2015 +, dostupna u starijim preglednicima ako koristite neku od mnogih obećanih knjižnica)

Sva tri su dostupna u trenutnim preglednicima i čvoru 7+.


ES2017 +: obećanja s async/await čekanje

ECMAScript verzija izdana 2017. godine uvela je sintaksnu podršku za asinkrone funkcije. S async i await možete pisati asinkrono u "sinkroni stil". Kôd je još uvijek asinkron, ali lakši za čitanje / razumijevanje.

async/await temelji se na obećanjima: async funkcija uvijek vraća obećanje. await "odmotati" obećanje i rezultat u smislu značenja obećanja riješen je ili daje pogrešku ako je obećanje odbijeno.

Važno: Možete koristiti samo await unutar async funkcije. Upravo sada, na najvišoj razini, await još nije podržan, tako da ćete možda morati učiniti da asinkroni IIFE pokrene async kontekst.

Možete pročitati više o async i await na MDN.

Evo primjera koji se temelji na gore navedenom kašnjenju:

 // 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); })(); 

Trenutne verzije preglednika i podrške hosta async/await . Također možete održavati starija okruženja pretvaranjem koda u ES5 pomoću regeneratora (ili alata koji koriste regenerator, kao što je Babel ).


Neka funkcije prihvaćaju povratne pozive.

Povratni poziv je jednostavno funkcija koja se prenosi na drugu funkciju. Ova druga funkcija može pozvati funkciju koja je prošla kad god je spremna. U kontekstu asinkronog procesa, povratni poziv će biti pozvan kad god se izvrši asinkroni proces. Obično se rezultat šalje pozivnom pozivu.

U primjeru pitanja možete prisiliti foo da prihvati povratni poziv i upotrijebite ga kao povratni poziv za success . Dakle, ovo

 var result = foo(); // Code that depends on 'result' 

To je sve

 foo(function(result) { // Code that depends on 'result' }); 

Ovdje definiramo funkciju "inline", ali možete proslijediti bilo koju referencu na funkciju:

 function myCallback(result) { // Code that depends on 'result' } foo(myCallback); 

Sam foo definiran je na sljedeći način:

 function foo(callback) { $.ajax({ // ... success: callback }); } 

callback odnosi se na funkciju koju prelazimo na foo kada je nazovemo i jednostavno je proslijedimo success . tj čim Ajax zahtjev bude uspješan, $.ajax će pozvati callback i $.ajax odgovor na povratni poziv (koji se može referencirati korištenjem result , jer smo tako definirali povratni poziv).

Također možete obraditi odgovor prije slanja poziva na povratni poziv:

 function foo(callback) { $.ajax({ // ... success: function(response) { // For example, filter the response callback(filtered_response); } }); } 

Pisanje koda pomoću povratnih poziva lakše je nego što se čini. Uostalom, JavaScript u pregledniku uvelike ovisi o događajima (DOM događajima). Dobivanje Ajax odgovora nije ništa više od događaja.
Poteškoće se mogu pojaviti kada morate raditi s kodom treće strane, ali većina problema se može riješiti jednostavnim promišljanjem tijeka aplikacije.


ES2015 +: obećanja s tada ()

Promise API je nova značajka ECMAScript 6 (ES2015), ali već ima dobru podršku za preglednike . Također postoji mnogo knjižnica koje implementiraju standardni API obećanja i pružaju dodatne metode za pojednostavljenje korištenja i sastavljanja asinkronih funkcija (na primjer, bluebird ).

Obećanja su kontejneri za buduće vrijednosti. Kada obećanje primi vrijednost (dopušteno) ili kada je otkazano (odbijeno), obavještava sve svoje "slušatelje" koji žele pristupiti toj vrijednosti.

Prednost u odnosu na jednostavne povratne pozive je u tome što vam omogućuju da odvojite svoj kod i lakše je sastaviti ga.

Evo jednostavnog primjera korištenja obećanja:

 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). }); 

S obzirom na naš poziv iz Ajaxa, možemo koristiti sljedeća obećanja:

 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 svih prednosti koje obećavaju da će ponuditi je izvan dosega ovog odgovora, ali ako pišete novi kod, trebali biste ih ozbiljno razmotriti. Oni pružaju izvrsnu apstrakciju i odvajanje vašeg koda.

Više informacija o obećanjima: HTML5 - stijene - JavaScript obećanja

Napomena: jQuery objekti na čekanju

Odloženi objekti su prilagođena implementacija obećanja u jQuery (prije standardizacije API-ja Promise). Skoro se ponašaju kao obećanja, ali razotkrivaju malo drugačiji API.

Svaka jQuery Ajax metoda već vraća "objekt na čekanju" (zapravo obećanje objekta na čekanju), kojeg možete jednostavno vratiti iz njegove funkcije:

 function ajax() { return $.ajax(...); } ajax().done(function(result) { // Code depending on result }).fail(function() { // An error occurred }); 

Napomena: obećanje je ispalo

Zapamtite da su obećanja i odgođeni predmeti samo kontejneri za buduću vrijednost, a ne sama vrijednost. Na primjer, pretpostavimo da imate sljedeće:

 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 } 

Ovaj kod pogrešno razumije gore navedene asinkrone probleme. Konkretno, $.ajax() ne zamrzava kôd prilikom provjere stranice '/ password' na vašem poslužitelju - šalje zahtjev poslužitelju i, dok čeka, odmah vraća jQuery Ajax Deferred objekt, a ne odgovor od poslužitelja. To znači da će if uvijek primiti taj odgođeni objekt, obraditi ga kao true i djelovati kao da je korisnik prijavljen. Nije dobro

Ali popravite to jednostavno:

 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 }); 

Ne preporučuje se: sinkroni pozivi "Ajax"

Kao što sam rekao, neke (!) Asinkrone operacije imaju sinkrone kopije. Ne zagovaram njihovu upotrebu, ali zbog cjelovitosti, evo kako treba izvršiti sinkroni poziv:

Nema jQuery

Ako izravno koristite objekt XMLHTTPRequest , proslijedite false kao treći argument .open .

jQuery

Ako koristite jQuery , async parametar možete postaviti na false . Imajte na umu da je ova opcija zastarjela još od jQuery 1.8. Tada još uvijek možete koristiti povratni poziv za success ili pristup svojstvu responseText jqXHR objekta :

 function foo() { var jqXHR = $.ajax({ //... async: false }); return jqXHR.responseText; } 

Ako koristite bilo koju drugu jQuery Ajax metodu, kao što su $.get , $.getJSON , itd., Trebate je promijeniti u $.ajax (budući da konfiguracijske parametre možete prenijeti samo na $.ajax ).

Pazi Nije moguće napraviti sinkroni JSONP zahtjev. JSONP je u prirodi uvijek asinkrona (još jedan razlog da ne razmotri ovu opciju).

5011
08 янв. Odgovor je dao Felix Kling, 08. siječnja 2013-01-08 20:06 '13 u 20:06 2013-01-08 20:06

Ako ne koristite jQuery u svom kodu, ovaj odgovor je za vas.

Kôd bi trebao biti nešto slično ovome:

 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 je izvrsno napisao odgovor za ljude koji koriste JQuery za AJAX, odlučio sam pružiti alternativu ljudima koji to ne čine.

( Napominjemo da sam onima koji koriste novi API za fetch , Angular ili obećanja, u nastavku dodao još jedan odgovor )


Što susrećete

Ovo je kratak sažetak "Objašnjenja problema" iz drugog odgovora, a ako niste sigurni, nakon čitanja ovoga, pročitajte ovo.

A u AJAX-u znači asinkrono . To znači da je slanje zahtjeva (ili, prije, primanje odgovora) uklonjeno iz normalnog tijeka izvršenja. U vašem primjeru, .send se odmah vraća, a sljedeći izraz return result; izvršava se prije nego što se čak i pozove funkcija koju ste poslali kao povratni poziv za success .

To znači da kada se vratite, slušatelj koji ste naveli još nije ispunjen, što znači da vrijednost koju ste vratili nije određena.

Jednostavna analogija

 function getFive(){ var a; setTimeout(function(){ a=5; },10); return a; } 

(Gusle)

Povratna vrijednost a je undefined , budući da dio a=5 još nije izvršen. AJAX to čini: vraćate vrijednost prije nego što je poslužitelj uspio reći vašem pregledniku koja je vrijednost.

Jedno od mogućih rješenja ovog problema je ponovno korištenje koda, informiranje vašeg programa o tome što učiniti kad se izračun dovrši.

 function onComplete(a){ // When the code completes, do this alert(a); } function getFive(whenDone){ var a; setTimeout(function(){ a=5; whenDone(a); },10); } 

To se naziva CPS . U osnovi, mi getFive akciju koju treba izvršiti kada završi, kažemo našem kodu kako reagirati kada se događaj završi (na primjer, naš AJAX poziv ili u ovom slučaju time-out).

Upotreba:

 getFive(onComplete); 

Koji bi trebao upozoriti "5" na zaslonu. (Fiddle) .

Moguća rješenja

border=0

Postoje dva načina rješavanja ovog problema:

  • Napravite sinkronizirani AJAX poziv (nazovite ga SJAX).
  • Restruktirajte svoj kôd kako biste ispravno radili s povratnim pozivima.

1. Sinkroni AJAX - nemojte to učiniti!

Što se tiče sinkronog AJAX-a, nemojte to učiniti! Felixov odgovor daje neke snažne argumente zašto je to loša ideja. Da sumiramo, zamrznut će korisnikov preglednik sve dok poslužitelj ne vrati odgovor i stvori vrlo loše korisničko sučelje. Evo još jednog MDN sažetka zašto:

XMLHttpRequest podržava sinkroniziranu i asinkronu komunikaciju. Općenito, međutim, asinhroni zahtjevi trebali bi biti povoljniji od sinkronih zahtjeva zbog razloga izvedbe.

Ukratko, sinkroni zahtjevi za izvršavanje blok koda ... mogu uzrokovati ozbiljne probleme

Ako to trebate učiniti, možete proslijediti oznaku: Evo kako:

 var request = new XMLHttpRequest(); request.open('GET', 'yourURL', false); // `false` makes the request synchronous request.send(null); if (request.status === 200) {// That HTTP for 'ok' console.log(request.responseText); } 

2. Kodeks restrukturiranja

Neka vaša funkcija prihvati povratni poziv. U primjeru, foo kôd može biti prihvaćen za povratni poziv. Reći ćemo naš kod kako reagirati kada foo završi.

Dakle:

 var result = foo(); // code that depends on `result` goes here 

postaje:

 foo(function(result) { // code that depends on `result` }); 

Ovdje smo prošli anonimnu funkciju, ali smo mogli jednostavno prenijeti vezu na postojeću funkciju, čineći je ovako:

 function myHandler(result) { // code that depends on `result` } foo(myHandler); 

Za više informacija o načinu izvršavanja ovog povratnog poziva, provjerite Felixov odgovor.

Sada ćemo definirati foo sebe da djeluje u skladu s tim

 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(); } 

(Violina)

Sada naša funkcija foo prihvaća akciju koja se pokreće kada AJAX uspješno završi, to možemo nastaviti provjerom reagira li status odgovora 200 i djeluje u skladu s tim (stvoriti rukovatelj pogreškama, itd.). Djelotvorno rješenje našeg problema.

Ako još uvijek imate problema s razumijevanjem, pročitajte AJAX Vodič za početak rada u MDN-u.

953
30 мая '13 в 2:30 2013-05-30 02:30 Odgovor dao Benjamin Gruenbaum 30. svibnja '13 u 2:30 2013-05-30 02:30

XMLHttpRequest 2 (prvo pročitajte odgovore Benjamina Grünbauma i Felixa Klinga )

Ako ne koristite jQuery i želite dobiti lijep kratki XMLHttpRequest 2 koji radi u modernim preglednicima, kao iu mobilnim preglednicima, predlažem da ga koristite na sljedeći način:

 function ajax(a, b, c){ // URL, callback, just a placeholder c = new XMLHttpRequest; c.open('GET', a); c.onload = b; c.send() } 

Kao što možete vidjeti:

  1. Kraći je od svih ostalih navedenih funkcija.
  2. Povratni poziv se uspostavlja izravno (stoga nema nepotrebnih nepotrebnih zatvaranja).
  3. Postoje i druge situacije koje se ne sjećam da je XMLHttpRequest 1 neugodno.

Postoje dva načina da dobijete odgovor na ovaj Ajax poziv (tri pomoću imena varijable XMLHttpRequest):

Najjednostavnije:

 this.response 

Ili, ako iz nekog razloga bind() povratni poziv s klasom:

 e.target.response 

primjer:

 function callback(e){ console.log(this.response); } ajax('URL', callback); 

Ili (gore navedene bolje anonimne funkcije su uvijek problem):

 ajax('URL', function(e){console.log(this.response)}); 

Ne postoji ništa lakše.

Sada će neki ljudi vjerojatno reći da je bolje koristiti onreadystatechange ili čak ime XMLHttpRequest varijable. Ovo je pogrešno.

Istražite napredne značajke XMLHttpRequest

Svi moderni preglednici su podržani. I mogu potvrditi da koristim ovaj pristup jer postoji XMLHttpRequest 2. Nikada nisam imao problema u svim preglednicima koje koristim.

onreadystatechange je korisno samo ako želite dobiti zaglavlja u stanju 2.

Korištenje imena varijable XMLHttpRequest je još jedna velika pogreška, jer trebate ponovno nazvati unutar zatvarača onload / oreadystatechange, inače ste ga izgubili.


Sada, ako želite nešto složenije pomoću posta i FormData, tu funkciju možete jednostavno proširiti:

 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) } 

Opet ... ovo je vrlo kratka funkcija, ali ona prima i objavljuje.

Primjeri upotrebe:

 x(url, callback); // By default it get so no need to set x(url, callback, 'post', {'key': 'val'}); // No need to set post data 

Ili proslijedite cijeli element obrasca ( document.getElementsByTagName('form')[0] ):

 var fd = new FormData(form); x(url, callback, 'post', fd); 

Ili postavite neke prilagođene vrijednosti:

 var fd = new FormData(); fd.append('key', 'val') x(url, callback, 'post', fd); 

Kao što možete vidjeti, nisam implementirao sinkronizaciju ... ovo je loše.

Rekavši to ... zašto to ne učinite na jednostavan način?


Kao što je spomenuto u komentaru, korištenje pogreške sinkrono u potpunosti krši značenje odgovora. Što je dobar kratak način da ispravno koristite Ajax?

Obrađivač pogreške

 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); 

U gornjoj skripti imate upravljač za pogreške koji je statički definiran, tako da ne ugrožava funkciju. Rukovatelj greškama može se koristiti za druge funkcije.

Но чтобы действительно вывести ошибку, единственный способ - написать неправильный URL, и в этом случае каждый браузер выдает ошибку.