Як повернути відповідь від асинхронного виклику?

У мене є функція 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`. 
4687
08 янв. заданий Felix Kling 08 Січня. 2013-01-08 20:06 '13 о 20:06 2013-01-08 20:06
@ 39 відповідей
  • 1
  • 2

→ Для більш загального пояснення асинхронного поведінки на різних прикладах см. Чому моя змінна не змінюється після того, як я змінив її всередині функції? - асинхронна посилання на код

→ Якщо ви вже зрозуміли проблему, перейдіть до можливих рішень нижче.

Ця проблема

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

Замість очікування відповіді виконання триває негайно і виконується оператор після виклику Ajax. Щоб в кінцевому підсумку отримати відповідь, ви надаєте функцію, яка буде викликатися після отримання відповіді, зворотний виклик (зауважте що-небудь, "передзвоніть"). Будь-оператор, наступний за цим викликом, виконується до виклику на зворотній дзвінок.


Рішення (s)

Прийміть асинхронну природу JavaScript! Хоча деякі асинхронні операції надають синхронні аналоги (як і "Ajax"), їх зазвичай не рекомендується використовувати, особливо в контексті браузера.

Чому це погано, запитаєте ви?

JavaScript запускається в потоці призначеного для користувача інтерфейсу браузера, і будь-який тривалий процес блокує призначений для користувача інтерфейс, що робить його нечуйними. Крім того, існує верхня межа часу виконання для JavaScript, і браузер запитає користувача, чи продовжувати виконання чи ні.

Все це дійсно поганий користувальницький досвід. Користувач не зможе сказати, чи все працює нормально чи ні. Крім того, ефект буде гірше для користувачів з повільним підключенням.

Далі ми розглянемо три різних рішення, які будуються один на одному:

  • Обіцянки з async/await (ES2017 +, є в старих браузерах, якщо ви використовуєте транспортер або регенератор)
  • Зворотні виклики (популярні в вузлі)
  • Обіцянки за допомогою then() (ES2015 +, є в старих браузерах, якщо ви використовуєте одну з безлічі бібліотек обіцянок)

Всі три доступні в поточних браузерах і вузлі 7 +.


ES2017 +: обіцянки з async/await очікуванням

Версія ECMAScript, випущена в 2017 році, представила підтримку на рівні синтаксису для асинхронних функцій. За допомогою async і await ви можете писати асинхронно в "синхронному стилі". Код все ще асинхронний, але його легше читати / розуміти.

async/await ґрунтується на обіцянках: async функція завжди повертає обіцянку. await "розгортає" обіцянку і або результат в значенні обіцянка була вирішені з або видає помилку, якщо обіцянка була відхилена.

Важливо: ви можете використовувати await тільки всередині async функції. Прямо зараз, на вищому рівні await поки не підтримується, так що ви, можливо, доведеться зробити асинхронної IIFE почати 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); })(); current user // 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 ).


Нехай функції приймають зворотні виклики

Як передзвонити - це просто функція, передана іншій функції. Ця інша функція може викликати функцію, передану щоразу, коли вона готова. В контексті асинхронного процесу зворотний виклик буде викликатися щоразу, коли виконується асинхронний процес. Зазвичай результат передається зворотному виклику.

У прикладі з питанням ви можете змусити 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 - не що інше, як подія.
Складнощі можуть виникнути, коли вам доведеться працювати зі стороннім кодом, але більшість проблем можна вирішити, просто подумавши потік додатку.


ES2015 +: обіцянки з then ()

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

Щодо нашого викликом 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 - скелі - Обіцянки JavaScript

Примітка: відкладені об'єкти jQuery

Відкладені об'єкти - це призначена для користувача реалізація обіцянок в jQuery (до стандартизації API Promise). Вони ведуть себе майже як обіцянки, але виставляють трохи інший 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 якості третьої аргументу .open .

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 за своєю природою завжди асинхронні (ще одна причина, щоб навіть не розглядати цю опцію).

5011
08 янв. відповідь дан Felix Kling 08 Січня. 2013-01-08 20:06 '13 о 20:06 2013-01-08 20:06

Якщо ви не використовуєте jQuery в своєму коді, ця відповідь для вас

Ваш код повинен бути чимось на зразок цього:

 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, я вирішив надати альтернативу для людей, які цього не роблять.

( Зверніть увагу, що для тих, хто використовує новий API fetch , Angular або promises, я додав ще один відповідь нижче )


Те, з чим ви стикаєтеся

Це коротке резюме "Пояснення проблеми" з іншої відповіді, якщо ви не впевнені, прочитавши це, прочитайте це.

A в AJAX означає асинхронний. Це означає, що відправка запиту (або, скоріше, отримання відповіді) виймається зі звичайного потоку виконання. У вашому прикладі .send повертається негайно, а наступний оператор return result; виконується до того, як функція, яку ви передали в якості success зворотного виклику, була навіть називається.

Це означає, що коли ви повертаєтеся, слухач, який ви визначили, ще не виконав, що означає, що повертається вами значення не було визначено.

проста аналогія

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

(Fiddle)

Значення, що повертається a одно undefined , так як частина 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) .

можливі рішення

border=0

В основному є два способи вирішення цієї проблеми:

  • Зробіть виклик AJAX синхронним (подзвоніть йому SJAX).
  • Реструктурує свій код для правильної роботи з зворотними викликами.

1. Синхронний AJAX - не робіть цього !!

Що стосується синхронного 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 HTTP for 'ok' console.log(request.responseText); } 

2. Код реструктуризації

Нехай ваша функція приймає зворотний виклик. У прикладі код 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(); } 

(скрипка)

Тепер наша функція foo приймає дію, яке запускається, коли AJAX завершується успішно, ми можемо продовжити це, перевіривши, чи не відповідає статус відповіді 200 і діє відповідно (створіть обробник помилок і т.д.). Ефективне вирішення нашої проблеми.

Якщо вам все ще важко зрозуміти це прочитати посібник для початківців AJAX в MDN.

953
30 мая '13 в 2:30 2013-05-30 02:30 відповідь дан Benjamin Gruenbaum 30 травня '13 о 2:30 2013-05-30 2:30

XMLHttpRequest 2 (перш за все прочитайте відповіді Бенджаміна Грюнбаум і Фелікса Клінга )

Якщо ви не використовуєте 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() } 

Як ви бачите:

  1. Це коротше, ніж всі інші функції, перераховані.
  2. Як передзвонити встановлюється безпосередньо (тому ніяких зайвих непотрібних замикань).
  3. Є деякі інші ситуації, які я не пам'ятаю, які роблять XMLHttpRequest 1 дратівливим.

Є два способи отримати відповідь на цей виклик Ajax (три з використанням імені змінної XMLHttpRequest):

найпростіший:

 this.response 

Або, якщо з якоїсь причини ви bind() зворотний виклик з класом:

 e.target.response 

приклад:

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

Або (вищенаведений краще анонімні функції завжди проблема):

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

Немає нічого простішого.

Тепер деякі люди, ймовірно, скажуть, що краще використовувати onreadystatechange або навіть ім'я змінної XMLHttpRequest. Це не правильно.

Ознайомтеся з розширеними функціями XMLHttpRequest

Підтримуються всі * сучасні браузери. І я можу підтвердити, що я використовую цей підхід, оскільки існує XMLHttpRequest 2. У мене ніколи не було ніяких проблем у всіх браузерах, які я використовую.

onreadystatechange корисний, тільки якщо ви хочете отримати заголовки в стані 2.

Використання імені змінної 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) } 

Знову ж ... це дуже коротка функція, але вона отримує і публікує.

Приклади використання:

 x(url, callback); // By default it get so no need to set x(url, callback, 'post', {'key': 'val'}); // No need to set post data set x(url, callback); // By default it 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 синхронний повністю порушує сенс відповіді. Який хороший короткий спосіб правильно використовувати 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); 

У наведеному вище сценарії у вас є обробник помилок, який статично визначений, тому він не ставить під загрозу функцію. Оброблювач помилок може використовуватися і для інших функцій.

Але щоб дійсно вивести помилку, єдиний спосіб - написати неправильний URL, і в цьому випадку кожен браузер видає помилку.