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


5506

У мене є функція, 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`.

Відповіді:


5700

→ Для більш загального пояснення поведінки асинхронізації з різними прикладами див . - Асинхронна посилання на код

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

Проблема

У Ajax означає асинхронний . Це означає, що відправлення запиту (а точніше отримання відповіді) виводиться з нормального потоку виконання. У вашому прикладі, повертається негайно та наступне твердження, виконується до того, як функція, яку ви передали як зворотний виклик, навіть була викликана.$.ajaxreturn 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 виконується. Щоб отримати відповідь у підсумку, ви надаєте функцію, яку потрібно викликати, як тільки відповідь отримана, зворотний виклик (помічаєте щось? Передзвоніть ?). Будь-яка заява, що надходить після цього дзвінка, виконується до виклику зворотного дзвінка.


Рішення (и)

Отримати асинхронний характер 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);
})();

Поточна версія браузера та вузлів підтримується 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). Отримання відповіді «Аякс» - це не що інше, як подія.
Труднощі можуть виникнути, коли вам доведеться працювати зі стороннім кодом, але більшість проблем можна вирішити, просто продумавши потік додатків.


ES2015 +: Обіцяє тоді ()

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.

Кожен метод jQuery Ajax вже повертає "відкладений об'єкт" (фактично обіцянку відкладеного об'єкта), який ви можете просто повернути зі своєї функції:

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


74
@Pommy: Якщо ви хочете використовувати jQuery, ви повинні включити його. Будь ласка, зверніться до docs.jquery.com/Tutorials:Getting_Started_with_jQuery .
Фелікс Клінг

11
У Рішенні 1, під jQuery, я не міг зрозуміти цей рядок: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(Так, я розумію, що мій нік - це іронічний
тад

32
@gibberish: Мммм, я не знаю, як це можна зробити зрозумілішим. Ви бачите, як fooвикликається і функція передається їй ( foo(function(result) {....});)? resultвикористовується всередині цієї функції і є відповіддю на запит Ajax. Для посилання на цю функцію перший параметр foo викликається callbackі присвоюється successзамість анонімної функції. Отже, $.ajaxзателефонуємо, callbackколи запит був успішним. Я спробував це пояснити трохи більше.
Фелікс Клінг

43
Чат для цього питання мертвий, тому я не знаю, де запропонувати окреслені зміни, але пропоную: 1) Змінити синхронну частину на просту дискусію про те, чому це погано, не маючи приклад коду того, як це зробити. 2) Видаліть / об'єднайте приклади зворотного виклику, щоб лише показати більш гнучкий підхід Deferred, який, на мою думку, також може бути трохи простішим для тих, хто навчається Javascript.
Кріс Москіні

14
@Jessi: Я думаю, ви неправильно зрозуміли цю частину відповіді. Ви не можете використовувати, $.getJSONякщо ви хочете, щоб запит Ajax був синхронним. Однак вам не слід вимагати, щоб запит був синхронним, щоб це не стосувалося. Ви повинні використовувати зворотні дзвінки або обіцянки для обробки відповіді, як це пояснено раніше у відповіді.
Фелікс Клінг

1071

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

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


З чим ти стикаєшся

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

У AJAX означає асинхронний . Це означає, що відправлення запиту (а точніше отримання відповіді) виводиться з нормального потоку виконання. У вашому прикладі, повертається негайно та наступне твердження, виконується до того, як функція, яку ви передали як зворотний виклик, навіть була викликана..sendreturn 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) .

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

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

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

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

Що стосується синхронного AJAX, не робіть цього! Відповідь Фелікса викликає кілька переконливих аргументів про те, чому це погана ідея. Підсумовуючи це, він заморозить браузер користувача, поки сервер не поверне відповідь і створить дуже поганий досвід користувача. Ось ще один короткий підсумок, взятий з 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);
}

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

Для отримання більш детальної інформації про те, як робиться така конструкція зворотного дзвінка, дивіться відповідь Фелікса.

Тепер давайте визначимось з 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.


20
"синхронні запити блокують виконання коду і можуть просочувати пам'ять та події", як синхронний запит може просочити пам'ять?
Матвій Г

10
@MatthewG Я в цьому питанні додав щедрості , я побачу, що я можу вилавити . Я тим часом знімаю цитату з відповіді.
Бенджамін Груенбаум

17
Тільки для довідки, XHR 2 дозволяє використовувати onloadобробник, який запускається лише тоді, коли readyStateє 4. Звичайно, він не підтримується в IE8. (iirc, можливо, знадобиться підтвердження.)
Флоріан Маргайн

9
Ваше пояснення, як передавати анонімну функцію як зворотний дзвінок, є дійсним, але оманливим. Приклад var bar = foo (); просить визначити змінну, тоді як ваш запропонований foo (functim () {}); не визначає смугу
Robbie Averill

396

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. Він використовує нове завантаження (тому вам не потрібно перевіряти готовність & статус)
  4. Є деякі інші ситуації, які я не пам’ятаю, що роблять XMLHttpRequest 1 дратівливим.

Є два способи отримати відповідь на цей виклик Ajax (три за допомогою імені var 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змінної - ще одна велика помилка, оскільки вам потрібно виконати зворотний виклик усередині закритого / завантажуваногозміни закриття, якщо ви його втратили.


Тепер, якщо ви хочете чогось складнішого, використовуючи пост та 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'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);

Як бачите, я не реалізував синхронізацію ... це погано.

Сказавши це ... чому б не зробити це простим способом?


Як зазначалося в коментарі, використання синхронних помилок && повністю порушує точку відповіді. Який хороший короткий спосіб використовувати 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-адресу, і в цьому випадку кожен браузер видає помилку.

Обробники помилок, можливо, корисні, якщо ви встановите власні заголовки, встановіть responseType для блокування буфера масиву чи будь-якого іншого ...

Навіть якщо ви передасте "POSTAPAPAP" як метод, він не призведе до помилки.

Навіть якщо ви передасте "fdggdgilfdghfldj" як форматні дані, це не призведе до помилки.

У першому випадку помилка всередині displayAjax()Under this.statusTextякMethod 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');

Не існує іншого способу зробити це неасинхронним способом. (Так, із циклом setTimeout ... але серйозно?)

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

Тільки якщо у вас є сторінка, на якій ви завжди завантажуєте той самий XML / JSON або будь-яку іншу, вам потрібна лише одна функція. У цьому випадку трохи змініть функцію Ajax і замініть b на свою спеціальну функцію.


Наведені вище функції призначені для основного використання.

Якщо ви хочете розширити функцію ...

Так, ти можеш.

Я використовую безліч API і одна з перших функцій, яку я інтегрую в кожну сторінку HTML, - це перша функція Ajax у цій відповіді, лише з GET ...

Але з XMLHttpRequest 2 ви можете багато чого зробити:

Я створив менеджер завантажень (використовуючи діапазони з обох сторін з резюме, файлочитачем, файловою системою), різні перетворювачі розмірів зображень за допомогою полотна, заповнюйте веб-бази даних SQL з base64images та багато іншого ... Але в цих випадках слід створити функцію лише для цього призначення ... іноді вам потрібні буфери для блокування, масиви масивів, ви можете встановити заголовки, переосмислити міметик, і є багато іншого ...

Але питання тут полягає в тому, як повернути відповідь на Аякс ... (Я додав простий спосіб.)


15
Хоча ця відповідь є приємною (І всі ми любимо XHR2, а розміщення даних про файли та багаточастинні дані є абсолютно приголомшливим) - це показує синтаксичний цукор для розміщення XHR з JavaScript - ви можете розмістити це в публікації в блозі (мені б хотілося) або навіть у бібліотеці (не впевнений у назві x, ajaxчи це xhrможе бути приємніше :)). Я не бачу, як це стосується повернення відповіді від дзвінка AJAX. (хтось все-таки міг би робити var res = x("url")і не розуміти, чому це не працює;)). Зі сторони - було б здорово, якби ви повернулися cз методу, щоб користувачі могли зачепитись errorтощо
Benjamin Gruenbaum

25
2.ajax is meant to be async.. so NO var res=x('url')..У цьому і полягає вся суть цього питання та відповіді :)
Бенджамін Грюнбаум

3
чому в функціях є параметр 'c', якщо в першому рядку ви перезаписуєте будь-яке значення, яке воно мало? я щось пропускаю?
Брайан Х.

2
Ви можете використовувати параметри як заповнювач, щоб не писати кілька разів "var"
cocco

11
@cocco Отже, ви написали оманливий, нечитабельний код у відповіді ТА , щоб зберегти кілька натискань клавіш? Будь ласка, не робіть цього.
камінь

316

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

Це означає AngularJS, jQuery (з відкладеним), заміну нативного XHR (отримання), збереження EmberJS, BackboneJS або будь-яку бібліотеку вузлів, яка повертає обіцянки.

У вашому коді має бути щось таке:

function foo() {
    var data;
    // or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // result is always undefined no matter what.

Фелікс Клінг зробив чудову роботу, написавши відповідь для людей, які використовують jQuery з зворотними зворотами для AJAX. У мене є відповідь для рідного XHR. Ця відповідь призначена для загального використання обіцянок або на фронті, або на вихідному.


Основне питання

Модель паралельної JavaScript у браузері та на сервері з NodeJS / io.js є асинхронною та реактивною .

Щоразу, коли ви викликаєте метод, який повертає обіцянку, thenобробники є завжди виконуються асинхронно - тобто після коду під ними, який не є в .thenобробнику.

Це означає, що при поверненні dataвизначеного thenвами обробника ще не виконали. Це в свою чергу означає, що значення, яке ви повертаєте, не було встановлено на правильне значення в часі.

Ось проста аналогія цього питання:

    function getFive(){
        var data;
        setTimeout(function(){ // set a timer for one second in the future
           data = 5; // after a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

Значення dataєundefined оскільки data = 5частина ще не виконана. Він, ймовірно, виконається за секунду, але до того часу він не має значення для повернутого значення.

Оскільки операція ще не відбулася (AJAX, виклик сервера, IO, таймер), ви повертаєте значення до того, як запит отримав можливість сказати своєму коду, що це за значення.

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

Швидкий підсумок обіцянок

Обіцянка - цінність у часі . Обіцянки мають стан, вони починаються як очікувані без жодної цінності і можуть вирішуватись на:

  • Виконано значення, що обчислення завершено успішно.
  • відкинув значення, що обчислення не вдалося.

Обіцянка може змінити штати лише один раз, після чого вона назавжди залишиться в одному стані. Ви можете приєднати thenобробники до обіцянок витягнути їх значення та обробляти помилки. thenобробники дозволяють ланцюжок дзвінків. Обіцяння створюються за допомогою API, які повертають їх . Наприклад, більш сучасна заміна AJAX fetchабо $.getповернення jQuery .

Коли ми закликаємо .thenобіцянку і повертаємо щось із неї - ми отримуємо обіцянку за оброблене значення . Якщо ми повернемо іншу обіцянку, ми отримаємо дивовижні речі, але давайте тримаємо коней.

З обіцянками

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

function delay(ms){ // takes amount of milliseconds
    // returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // when the time is up
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

Тепер, після того, як ми перетворили setTimeout для використання обіцянок, ми можемо використовувати thenйого для підрахунку:

function delay(ms){ // takes amount of milliseconds
  // returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // when the time is up
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // we're RETURNING the promise, remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // when the promise is ready
      return 5; // return the value 5, promises are all about return values
  })
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){ 
   document.body.innerHTML = five;
});

В принципі, замість того , щоб повертати значення , яке ми не можемо зробити з моделі паралелізму - ми повертаємося в обгортку для значення , яке ми можемо UnWrap з then. Це як коробка, яку можна відкрити then.

Застосовуючи це

Це так само, як і для вашого оригінального дзвінка API, ви можете:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // process it inside the `then`
    });
}

foo().then(function(response){
    // access the value inside the `then`
})

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

ES2015 (ES6)

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

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

Це функція, яка повертає ітератор над послідовністю, 1,2,3,3,3,3,....яку можна повторити. Хоча це цікаво саме по собі і відкриває місце для великої кількості можливостей, є один особливо цікавий випадок.

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

Цей дещо хитрий, але дуже потужний трюк дозволяє нам записувати асинхронний код синхронно. Є кілька "бігунів", які роблять це за вас, написання одного - це короткий ряд рядків коду, але виходить за рамки цієї відповіді. Я буду Promise.coroutineтут використовувати Bluebird , але є й інші обгортки на кшталт coабо Q.async.

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
});

Цей метод повертає собі обіцянку, яку ми можемо споживати з інших процедур. Наприклад:

var main = coroutine(function*(){
   var bar = yield foo(); // wait our earlier coroutine, it returns a promise
   // server call done here, code below executes when done
   var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
   console.log(baz); // runs after both requests done
});
main();

ES2016 (ES7)

У ES7 це ще більше стандартизовано, зараз є кілька пропозицій, але в усіх з них ви можете awaitпообіцяти. Це просто "цукор" (приємніший синтаксис) для пропозиції ES6 вище, додавши ключові слова asyncта await. Складання наведеного вище прикладу:

async function foo(){
    var data = await fetch("/echo/json"); // notice the await
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
}

Це все одно повертає обіцянку так само :)


Це має бути прийнятою відповіддю. +1 для асинхронізації / очікування (хоча чи не варто return await data.json();?)
Льюїс Донован

247

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

Це є:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

Повернення нічого в обробник подачі нічого не зробить. Натомість ви повинні або здати дані, або робити все, що завгодно, безпосередньо у функції успіху.


13
Ця відповідь є повністю семантичним ... ваш метод успіху - це лише зворотний дзвінок у межах зворотного дзвінка. Ви могли просто мати, success: handleDataі це спрацювало б.
Жак ジ ャ ッ ク

5
А що, якщо ви хочете повернути "responseData" поза "handleData" ... :) ... як ви це зробите ...? ... тому що просте повернення поверне його до "успіху" зворотного виклику ajax ... а не за межами "handleData" ...
pesho hristov

@Jacques & @pesho hristov Ви пропустили цю точку. Надіслати обробник - це не successметод, це оточуюча сфера $.ajax.
травник

@travnik Я цього не пропустив. Якби ви взяли вміст handleData і ввели його в метод успіху, він діяв би точно так само ...
Jacques ジ ャ ッ ク

234

Найпростіше рішення - створити функцію JavaScript і викликати її для successзворотного дзвінка Ajax .

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);    
}); 

3
Я не знаю, хто проголосував негативно. Але це робота, навколо якої справді працював, я використовував цей підхід для створення цілого додатку. Jquery.ajax не повертає дані, тому краще використовувати вищевказаний підхід. Якщо це неправильно, то поясніть і запропонуйте кращий спосіб зробити це.
Хемант Бавле

11
Вибачте, я забув залишити коментар (зазвичай це роблю!). Я спростував це. Новинки не вказують на фактичну коректність чи відсутність, вони вказують на корисність у контексті чи відсутність. Я не вважаю вашу відповідь корисною з огляду на Фелікса, який уже пояснює це лише набагато детальніше. Що стосується бічної записки, то чому б ви накреслили відповідь, якщо це JSON?
Бенджамін Грюнбаум

5
Ок .. @Benjamin Я використовував stringify, щоб перетворити об'єкт JSON в рядок. І дякую за уточнення вашої точки зору. Майте на увазі, щоб розміщувати більш детальні відповіді.
Hemant Bavle

А що, якщо ви хочете повернути "responseObj" поза "successCallback" ... :) ... як ви це зробите ...? ... тому що просте повернення поверне його до "успіху" зворотного виклику ajax ... а не за межами "successCallback" ...
pesho hristov

221

Я відповім жахливим на вигляд комедійним малюнком. Друге зображення є причиною того, що resultє undefinedу вашому прикладі коду.

введіть тут опис зображення


32
Картина вартує тисячі слів , Особа A - Попросіть у людини деталей B, щоб виправити свою машину, в свою чергу Особа B - Здійснює Ajax Call і чекає відповіді з сервера на деталі виправлення автомобіля, коли відповідь отримана, функція Ajax Success викликає особу B функція і передає відповідь як аргумент їй, людина А отримує відповідь.
shaijut

10
Було б чудово, якби ви додали рядки коду до кожного зображення для ілюстрації понять.
Хассан Байг

1
Тим часом хлопець з машиною застряг на узбіччі дороги. Він вимагає, щоб автомобіль був зафіксований, перш ніж продовжувати. Зараз він один на узбіччі дороги, який чекає ... Він скоріше буде по телефону в очікуванні зміни статусу, але механік цього не зробить ... Механік сказав, що йому потрібно працювати зі своєю роботою і не може просто повісити по телефону. Механік пообіцяв, що передзвонить йому, як тільки зможе. Приблизно через 4 години хлопець здається і телефонує Uber. - Приклад тайм-ауту.
barrypicker

@barrypicker :-D Блискуче!
Йоганнес Фаренкруг

159

Кутовий1

Люди, які використовують AngularJS , можуть впоратися з цією ситуацією, використовуючи Promises.

Тут сказано:

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

Ви також можете знайти приємне пояснення тут .

Приклад знайдено в документах, згаданих нижче.

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      //Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved 
 // and its value will be the result of promiseA incremented by 1.

Кутовий2 та пізніші

Ознайомтесь Angular2із наступним прикладом, але його рекомендується використовувати Observablesразом із Angular2.

 search(term: string) {
     return this.http
  .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
  .map((response) => response.json())
  .toPromise();

}

Ви можете споживати це таким чином,

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

Дивіться оригінальний пост тут. Але Typescript не підтримує нативні обіцянки es6 , якщо ви хочете ним скористатися, вам може знадобитися плагін для цього.

Крім того, тут визначено специфікацію обіцянок .


15
Це не пояснює, як обіцянки взагалі вирішать це питання.
Бенджамін Груенбаум

4
jQuery і метод отримання також обидва повернення обіцянки. Я б запропонував переглянути вашу відповідь. Хоча jQuery не зовсім однаковий (тоді він є, але лов не).
Tracker1

153

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

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

Приклад:

Причина, яка не працює, полягає в тому, що зворотні виклики doSomethingAsyncще не запущені до того моменту, коли ви намагаєтесь використовувати результати.

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

Паралельний

Ви можете запустити їх і відслідковувати кількість очікуваних зворотних дзвінків, а потім використовувати результати, коли ви отримали стільки зворотних викликів:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

Приклад:

(Ми могли б покінчити з expectingта просто скористатися results.length === theArray.length, але це залишає нам відкриті можливості theArrayзмінити, поки дзвінки будуть невирішеними ...)

Зверніть увагу, як ми використовуємо indexз, forEachщоб зберегти результат у resultsтій же позиції, що і запис, до якого він стосується, навіть якщо результати виходять з ладу (оскільки виклики асинхронізації не обов'язково завершуються в тому порядку, в якому вони були запущені).

Але що робити, якщо вам потрібно повернути ці результати з функції? Як вказували інші відповіді, ви не можете; ви повинні прийняти свою функцію та прийняти зворотний дзвінок (або повернути Обіцянку ). Ось версія зворотного дзвінка:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

Приклад:

Або ось версія, що повертає Promiseнатомість:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Звичайно, якщо doSomethingAsyncми передали нам помилки, ми б використали, rejectщоб відхилити обіцянку, коли отримали помилку.)

Приклад:

(Або по черзі, ви можете зробити обгортку, doSomethingAsyncяка повертає обіцянку, а потім виконайте наведене нижче ...)

Якщо doSomethingAsyncви даєте обіцянку , ви можете використовувати Promise.all:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Якщо ви знаєте, що doSomethingAsyncбуде ігнорувати другий та третій аргументи, ви можете просто передати його безпосередньо map( mapвикликає зворотний виклик з трьома аргументами, але більшість людей використовує лише перший більшість часу):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Приклад:

Зауважте, що Promise.allрішення вашої обіцянки вирішує масив результатів усіх обіцянок, які ви даєте, коли всі вони виконані, або відхиляє її обіцянку, коли перша з обіцянок, яку ви даєте, відхиляє.

Серія

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

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(Оскільки ми робимо роботу послідовно, ми можемо просто використовувати, results.push(result)оскільки знаємо, що результати не вийде з ладу. У вищесказаному ми могли б використати results[index] = result;, але в деяких із наведених нижче прикладів у нас немає індексу використовувати.)

Приклад:

(Або, знову ж таки, складіть обгортку для doSomethingAsyncцього, що дає вам обіцянку, і виконайте наступне ...)

Якщо doSomethingAsyncви даєте обіцянку, якщо ви можете використовувати синтаксис ES2017 + (можливо, з транспілятором, як Babel ), ви можете використовувати asyncфункцію з for-ofі await:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Приклад:

Якщо ви не можете використовувати синтаксис ES2017 + (поки що), ви можете скористатися варіантом шаблону "Обіцяти зменшення" (це складніше, ніж звичайне зменшення обіцянки, оскільки ми не передаємо результат з одного в інший, а натомість збираючи їх результати в масив):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Приклад:

... що менш громіздко за функціями ES2015 + стрілки :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Приклад:


1
Чи можете ви пояснити, як працює if (--expecting === 0)частина коду? Версія для зворотного дзвінка вашого рішення працює для мене чудово, я просто не розумію, як за допомогою цього твердження ви перевіряєте кількість виконаних відповідей. Вдячний, що з мого боку це просто відсутність знань. Чи є альтернативний спосіб, щоб можна було написати чек?
Сара

@Sarah: expectingпочинається зі значення array.length, яке кількість запитів ми будемо робити. Ми знаємо, що зворотний виклик не буде зателефоновано, поки не будуть запущені всі ці запити. У зворотному дзвінку if (--expecting === 0)чи так: 1. Зменшення expecting(ми отримали відповідь, тому ми очікуємо на одну меншу кількість відповідей), і якщо значення після зменшення дорівнює 0 (ми не очікуємо більше відповідей), ми зроблено!
TJ Crowder

1
@PatrickRoberts - Дякую !! Так, помилка копіювання та вставки, цей другий аргумент був повністю проігнорований у цьому прикладі (це єдина причина, що він не вийшов з ладу, оскільки, як ви вказали, resultsне існувало). :-) Виправлено.
TJ Crowder

111

Подивіться на цей приклад:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

Як бачите, getJokeце повернення вирішеної обіцянки (це вирішується при поверненні res.data.value). Отже, ви зачекаєте, поки запит $ http.get буде завершений, а потім буде виконано console.log (res.joke) (як звичайний асинхронний потік).

Це plnkr:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

Шлях ES6 (асинхронізація - чекаю)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();

107

Це одне з місць, яке двома способами прив'язки даних або збереженням концепції, яка використовується у багатьох нових рамках JavaScript, буде чудово працювати для вас ...

Отже, якщо ви використовуєте Angular, React або будь-які інші рамки, які роблять два способи прив'язки даних або зберігають концепцію, ця проблема для вас просто виправлена, тож простим словом, ваш результат знаходиться undefinedна першому етапі, тому у вас є ще result = undefinedдо отримання Дані, тоді, як тільки ви отримаєте результат, він буде оновлений і отримає призначення до нового значення, яке відповість на ваш виклик Ajax ...

Але як це можна зробити, наприклад, у чистому JavaScript або jQuery, як ви задали це запитання?

Ви можете використовувати зворотний дзвінок , обіцянку та нещодавнє спостереження, щоб обробити його для вас, наприклад, у обіцянках у нас є якась функція, яка success()або then()яка буде виконана, коли ваші дані будуть готові для вас, як і функція зворотного дзвінка або функція підписки під час спостереження .

Наприклад, у вашому випадку, у якому ви використовуєте jQuery , ви можете зробити щось подібне:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

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


Це добре в глобальному масштабі, але в якомусь контексті модуля ви, мабуть, хочете забезпечити правильний контекст для зворотного дзвінка, наприклад$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims

8
Це насправді неправильно, оскільки React є односторонньою прив'язкою даних
Меттью Брент

@MatthewBrent Ви не помиляєтеся, але не так також. Реквізити React є об'єктом, і якщо вони змінені, вони змінюються протягом усього додатка, але це не спосіб, який розробник React рекомендує використовувати ним ...
Аліреза

98

Це дуже поширене питання, з яким ми стикаємось, боремося з «таємницями» JavaScript. Дозвольте сьогодні спробувати демістифікувати цю таємницю.

Почнемо з простої функції JavaScript:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

Це простий синхронний виклик функції (коли кожен рядок коду "закінчується своїм завданням" перед наступним послідовним послідовністю), і результат такий же, як і очікувався.

Тепер додамо трохи повороту, запровадивши невелику затримку в нашій функції, щоб усі рядки коду не були «закінчені» послідовно. Таким чином, він буде імітувати асинхронну поведінку функції:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

Отже, ця затримка просто зламала функціонал, який ми очікували! Але що саме сталося? Ну, це насправді досить логічно, якщо подивитися на код. foo()Після виконання функція нічого не повертає (таким чином повернене значення є undefined), але він запускає таймер, який виконує функцію після 1s, щоб повернути "wohoo". Але, як ви бачите, значення, яке присвоюється бар, - це негайно повернені речі з foo (), що не є нічого, тобто просто undefined.

Отже, як ми вирішуємо це питання?

Давайте запитаємо нашу функцію для ПРОМІЗИ . Обіцяння справді стосується того, що це означає: це означає, що функція гарантує вам забезпечити будь-який вихід, який він отримає в майбутньому. тож давайте подивимось це в дії нашої маленької проблеми вище:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

Таким чином, підсумок - для вирішення асинхронних функцій, таких як дзвінки на базі ajax тощо, ви можете використовувати обіцянку до resolveзначення (яке ви збираєтесь повернути). Таким чином, коротше кажучи, ви вирішите значення замість повернення в асинхронних функціях.

ОНОВЛЕННЯ (Обіцяння з асинхронізацією / очікуванням)

Крім використання then/catchроботи з обіцянками, існує ще один підхід. Ідея полягає в тому, щоб розпізнати асинхронну функцію, а потім дочекатися вирішення обіцянок , перш ніж перейти до наступного рядка коду. Це все ще лише promisesпід кришкою, але з іншим синтаксичним підходом. Щоб зробити речі зрозумілішими, ви можете знайти порівняння нижче:

тоді / ловити версію:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

версія асинхронізації / очікування:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }

це все ще вважається найкращим способом повернути значення з обіцянки чи асинхронізації / очікування?
edwardsmarkf

3
@edwardsmarkf Особисто я не думаю, що існує найкращий спосіб. Я використовую обіцянки з функцією then / catch, async / wait, а також генератори для асинхронних частин свого коду. Це багато в чому залежить від контексту використання.
Аніш К.

96

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

Ось приклад того ж:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

Я використовую resultоб'єкт для зберігання значення під час асинхронної операції. Це дозволяє отримати результат навіть після асинхронного завдання.

Я дуже використовую такий підхід. Мені було б цікаво дізнатись, наскільки добре працює цей підхід, коли залучається результат назад через послідовні модулі.


9
Тут немає нічого особливого у використанні об’єкта. Було б добре, якби ви призначили йому відповідь безпосередньо result. Це працює, тому що ви читаєте змінну після завершення функції асинхронізації.
Фелікс Клінг

85

Незважаючи на те, що обіцянки та зворотні дзвінки спрацьовують добре у багатьох ситуаціях, болі в тилу висловити щось на кшталт:

if (!name) {
  name = async1();
}
async2(name);

Ви б закінчилися async1; перевірте, чи nameне визначено це чи ні, і зателефонуйте відповідно до зворотного дзвінка.

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

Незважаючи на те, що в невеликих прикладах це нормально, це дратує, коли у вас багато подібних випадків та помилок.

Fibers допомагає у вирішенні питання.

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

Оформити замовлення можна тут .


1
@recurf - Це не мій проект. Ви можете спробувати скористатися їх трекером випуску.
rohithpr

1
це схоже на функції генератора? developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… *
Emanegux

1
Це все ще актуально?
Алуан Хаддад

Ви можете скористатися, async-awaitякщо ви використовуєте деякі новітні версії вузла. Якщо хтось застряг із старими версіями, він може скористатися цим методом.
rohithpr

83

Наступний приклад, який я написав, показує, як це зробити

  • Обробляти асинхронні дзвінки HTTP;
  • Чекайте відповіді від кожного дзвінка API;
  • Використовуйте схему обіцянки ;
  • Використовуйте шаблон Promise.all для приєднання до декількох HTTP-дзвінків;

Цей робочий приклад є самостійним. Він визначатиме простий об'єкт запиту, який використовує XMLHttpRequestоб’єкт вікна для здійснення дзвінків. Він визначить просту функцію, щоб чекати, коли купа обіцянок буде виконана.

Контекст. Приклад - запит кінцевої точки веб-API Spotify для пошуку playlistоб'єктів для заданого набору рядків запитів:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

Для кожного пункту нове Обіцяння буде блокувати блок - ExecutionBlock, аналізувати результат, планувати новий набір обіцянок на основі масиву результатів, тобто списку userоб’єктів Spotify та виконувати новий виклик HTTP в ExecutionProfileBlockасинхронному режимі.

Потім ви можете побачити вкладену структуру Promise, яка дозволяє нереститися декількома та повністю асинхронними вкладеними HTTP-дзвінками та приєднувати результати до кожного підмножини викликів Promise.all.

ПРИМІТКА. Для останніх searchAPI Spotify потрібно буде вказати маркер доступу в заголовках запиту:

-H "Authorization: Bearer {your access token}" 

Отже, для запуску наступного прикладу потрібно помістити маркер доступу в заголовки запиту:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

Я широко обговорював це рішення тут .


80

Коротка відповідь полягає в тому, що вам потрібно реалізувати зворотний виклик так:

function callback(response) {
    // Here you can do what ever you want with the response object.
    console.log(response);
}

$.ajax({
    url: "...",
    success: callback
});

78

Відповідь 2017 року: тепер ви можете робити саме те, що хочете, у кожному поточному браузері та вузлі

Це досить просто:

  • Поверніть обіцянку
  • Скористайтеся функцією "очікувати" , яка скаже JavaScript, щоб очікувати, коли обіцянка буде вирішена у значення (наприклад, відповідь HTTP)
  • Додайте ключове слово "async" до батьківської функції

Ось робоча версія вашого коду:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

await підтримується у всіх поточних браузерах та вузлі 8


7
На жаль, це працює лише з функціями, які повертають обіцянки - наприклад, він не працює з API Node.js, який використовує зворотні виклики. І я б не рекомендував використовувати його без Babel, оскільки не всі користуються "поточними браузерами".
Michał Perłakowski

2
@ MichałPerłakowski вузол 8 включає nodejs.org/api/util.html#util_util_promisify_original, який може бути використаний для здійснення обіцянки повернення API node.js. Від того, чи є у вас час і гроші на підтримку поточних браузерів, очевидно, залежить від вашої ситуації.
mikemaccana

IE 11 все ще є поточним браузером у 2018 році, на жаль, і він не підтримуєawait/async
Juan Mendes

IE11 - це не поточний браузер. Він був випущений 5 років тому, має частку на світовому ринку 2,5% за даними caniuse, і якщо хтось не подвоює ваш бюджет, щоб ігнорувати всі поточні технології, то часу для більшості людей це не варто.
mikemaccana

76

Js - це одна різьба.

Веб-переглядач можна розділити на три частини:

1) Петля події

2) Веб-API

3) Черга подій

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

Тепер давайте подумаємо, що ми натиснули дві функції в черзі, одна - для отримання даних з сервера, а інша використовує ці дані. Ми спочатку натиснули функцію serverRequest () у чергу, потім функцію utiliseData (). Функція serverRequest переходить у цикл подій і здійснює дзвінок на сервер, оскільки ми ніколи не знаємо, скільки часу знадобиться для отримання даних з сервера, тому очікується, що цей процес потребує часу, і тому ми зайняли наш цикл подій, таким чином повісивши нашу сторінку, ось тут і веб API вступає в роль, він бере цю функцію з циклу подій і займається тим, що сервер робить цикл подій безкоштовним, щоб ми могли виконувати наступну функцію з черги. Наступною функцією в черзі є utiliseData (), яка переходить у цикл, але через відсутність даних вона переходить відпрацьовування та виконання наступної функції триває до кінця черги (це називається Async call, тобто ми можемо зробити щось інше, поки не отримаємо дані)

Припустимо, що наша функція serverRequest () мала в коді оператор повернення, коли ми повернемо дані з сервера Web API поштовхне їх у чергу в кінці черги. Оскільки його натискають в кінці черги, ми не можемо використовувати його дані, оскільки в нашій черзі не залишилося жодної функції для використання цих даних. Таким чином, повернути щось з Async Call неможливо.

Таким чином, рішення цього є зворотний дзвінок або обіцянка .

Зображення з однієї з відповідей тут. Правильно пояснюється використання зворотного виклику ... Ми надаємо нашу функцію (функцію, що використовує дані, повернуті з сервера) для функціонування сервера виклику.

CallBack

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

У моєму кодексі це називається як

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Зворотний виклик Javscript.info


68

Ви можете використовувати цю власну бібліотеку (написану за допомогою Promise) для здійснення віддаленого дзвінка.

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

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

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});

67

Іншим рішенням є виконання коду за допомогою послідовного виконавця nsynjs .

Якщо основна функція обіцяна

nsynjs буде оцінювати всі обіцянки послідовно та додавати результат обіцянки у dataвластивість:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Якщо основна функція не обіцяна

Крок 1. Оберніть функцію із зворотним викликом у обгортку, відому nsynjs (якщо вона має обіцяну версію, ви можете пропустити цей крок):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

Крок 2. Введіть синхронну логіку у функцію:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

Крок 3. Запустіть функцію синхронно за допомогою nsynjs:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

Nsynjs оцінить всі оператори та вирази поетапно, призупинивши виконання у випадку, якщо результат якоїсь повільної функції не готовий.

Більше прикладів тут: https://github.com/amaksr/nsynjs/tree/master/examples


2
Це цікаво. Мені подобається, як це дозволяє кодувати асинхронні виклики так, як ви це робили б іншими мовами. Але технічно це не справжній JavaScript?
Дж. Морріс

41

ECMAScript 6 має "генератори", які дозволяють легко програмувати в асинхронному стилі.

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

Щоб запустити вищевказаний код, зробіть це:

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

Якщо вам потрібно націлити браузери, які не підтримують ES6, ви можете запустити код через Babel або компілятор закриття для створення ECMAScript 5.

Зворотний виклик ...argsзагортається в масив і руйнується, коли ви їх читаєте, щоб шаблон міг впоратися із зворотними дзвінками, що мають кілька аргументів. Наприклад, з вузлом fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);

39

Ось кілька підходів до роботи з асинхронними запитами:

  1. Об’єкт обіцянки браузера
  2. Q - Бібліотека обіцянок для JavaScript
  3. A + Promises.js
  4. jQuery відкладено
  5. API XMLHttpRequest
  6. Використання концепції зворотного виклику - як реалізація у першій відповіді

Приклад: jQuery відкладена реалізація для роботи з декількома запитами

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();


38

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

Комп'ютерні системи, які ми будуємо - все більше і більше - мають час як важливий вимір. Певні речі налаштовані так, щоб вони відбувалися в майбутньому. Тоді й інші речі повинні відбуватися після того, як ці перші речі врешті відбудуться. Це основне поняття під назвою "асинхронність". У нашому все більш мережевому світі найпоширенішим випадком асинхронності є очікування, коли якась віддалена система відповість на якийсь запит.

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

var milk = order_milk();
put_in_coffee(milk);

Тому що JS не має можливості дізнатися , що потрібно чекати для order_milkдо кінця перед виконанням put_in_coffee. Іншими словами, він не знає, що асинхроннийorder_milk - це те, що не збирається домогтися молока до деякого майбутнього часу. JS та інші декларативні мови виконують одне твердження за іншим, не чекаючи.

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

order_milk(put_in_coffee);

order_milk стартує, замовляє молоко, тоді, коли і лише коли воно прибуває, воно викликає put_in_coffee .

Проблема цього підходу до зворотного виклику полягає в тому, що він забруднює нормальну семантику функції, з якою повідомляє про свій результат return; натомість функції не повинні повідомляти про свої результати, викликаючи зворотний виклик, заданий як параметр. Також такий підхід може швидко стати непростим при роботі з довшими послідовностями подій. Наприклад, скажімо, що я хочу зачекати, коли молоко почне в каву, а потім і лише потім виконувати третій крок, а саме випити каву. Зрештою, мені потрібно написати щось подібне:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

де я переходжу до put_in_coffeeмолока, щоб вставити його, а також дії ( drink_coffee) виконувати, коли молоко було введено. Такий код стає важко писати, читати та відладжувати.

У цьому випадку ми можемо переписати код у запитання як:

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

Введіть обіцянки

Це було мотивацією для поняття "обіцянка", що є особливим типом цінності, яке представляє якийсь майбутній або асинхронний результат. Він може представляти щось, що вже відбулося, або що відбудеться в майбутньому, або взагалі ніколи не відбудеться. Обіцяння мають єдиний метод, названий then, до якого ви передаєте дію, яку слід виконати, коли результат, який обіцянка представляє, був реалізований.

У разі нашого молока і кави, ми проектуємо , order_milkщоб повернути обіцянку в молоко прибуває, а потім вказати в put_in_coffeeякості thenдії, таким чином :

order_milk() . then(put_in_coffee)

Однією з переваг цього є те, що ми можемо об'єднати їх разом, щоб створити послідовності майбутніх подій ("ланцюжок"):

order_milk() . then(put_in_coffee) . then(drink_coffee)

Давайте застосуємо обіцянки до вашої конкретної проблеми. Ми зафіксуємо нашу логіку запиту всередині функції, яка повертає обіцянку:

function get_data() {
  return $.ajax('/foo.json');
}

Насправді все, що ми зробили, - це додавання returnдо дзвінка $.ajax. Це працює, тому що jQuery $.ajaxвже повертає щось на зразок обіцянки. (На практиці, не вникаючи в деталі, ми вважаємо за краще завершити цей виклик так, щоб повернути реальну обіцянку, або використати якусь альтернативу $.ajaxцьому.) Тепер, якщо ми хочемо завантажити файл і чекати, коли він закінчиться і тоді щось робити, ми можемо просто сказати

get_data() . then(do_something)

наприклад,

get_data() . 
  then(function(data) { console.log(data); });

Використовуючи обіцянки, ми закінчуємо передачу безлічі функцій then, тому часто корисно використовувати більш компактні функції стрілок у стилі ES6:

get_data() . 
  then(data => console.log(data));

asyncключове слово

Але все ще є щось нечітко незадоволення в тому, що потрібно писати код в один бік, якщо синхронний, і зовсім по-іншому, якщо асинхронний. Для синхронних записуємо

a();
b();

але якщо aце асинхронно, ми повинні писати обіцянки

a() . then(b);

Вище ми говорили: "JS не має можливості знати, що йому потрібно дочекатися закінчення першого дзвінка, перш ніж він виконає другий". Не було б непогано, якби був якийсь спосіб сказати JS? Виявляється, є - awaitключове слово, яке використовується всередині спеціального типу функції, що називається функцією "асинхронізація". Ця функція є частиною майбутньої версії ES, але вона вже доступна в таких транспіляторах, як Babel, які мають правильні пресети. Це дозволяє нам просто писати

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

У вашому випадку ви могли б написати щось подібне

async function foo() {
  data = await get_data();
  console.log(data);
}

37

Коротка відповідь : Ваш foo()метод повертається негайно, тоді як $ajax()виклик виконується асинхронно після повернення функції . Проблема полягає в тому, як і де зберігати результати, отримані за допомогою виклику async, як тільки вони повертаються.

У цій темі було дано кілька рішень. Мабуть, найпростіший спосіб - передавати об’єкт foo()методу та зберігати результати в члені цього об’єкта після завершення виклику асинхронізації.

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

Зауважте, що заклик до foo()все одно не поверне нічого корисного. Однак результат виклику асинхронізації тепер буде збережено в result.response.


14
Хоча це працює, це насправді не краще, ніж присвоєння глобальній змінній.
Фелікс Клінг

36

Використовуйте callback()функцію всередині foo()успіху. Спробуйте таким чином. Це просто і легко зрозуміти.  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();

29

Питання:

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

що МОЖНА трактувати як:

Як зробити так, щоб асинхронний код виглядав синхронним ?

Рішення буде уникати зворотних дзвінків, а також використовувати комбінацію Обіцянь та асинхронізації / очікування .

Я хотів би навести приклад для запиту Ajax.

(Хоча це може бути написано на Javascript, я вважаю за краще писати його на Python і компілювати його в Javascript за допомогою Transcrypt . Це буде досить зрозуміло.)

Дозволяє спочатку ввімкнути використання JQuery, щоб мати $доступ як S:

__pragma__ ('alias', 'S', '$')

Визначте функцію, яка повертає Обіцянку , у цьому випадку виклик Ajax:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

Використовуйте асинхронний код як би синхронним :

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")

29

Використання обіцянки

Найдосконаліша відповідь на це питання - використання Promise.

function ajax(method, url, params) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open(method, url);
    xhr.send(params);
  });
}

Використання

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

Але чекай ...!

Проблема з використанням обіцянок!

Чому ми повинні використовувати власну власну Обіцянку?

Я деякий час використовував це рішення, поки не зрозумів, що в старих браузерах є помилка:

Uncaught ReferenceError: Promise is not defined

Тому я вирішив реалізувати свій власний клас Promise для ES3 нижче компіляторів js, якщо його не визначено. Просто додайте цей код перед основним кодом, а потім безпечно використовуйте Promise!

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}

28

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

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}

5
Щодо зворотних зворотних дзвінків чи JavaScript нічого не є асинхронним.
Алуан Хаддад

19

Замість того, щоб кидати вам код, є дві концепції, які є ключовими для розуміння того, як JS обробляє зворотні дзвінки та асинхронність. (це навіть слово?)

Цикл подій та модель одночасності

Є три речі, про які потрібно пам’ятати; Черга; цикл події та стек

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

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

Як тільки він отримує повідомлення про запуск чогось, він додає його до черги. Черга - це список речей, які очікують на виконання (наприклад, ваш запит AJAX). уявіть це так:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

Коли одне з цих повідомлень буде виконано, воно спливає повідомлення з черги та створює стек, стек - це все, що потрібно JS виконати, щоб виконати інструкцію в повідомленні. Тож у нашому прикладі сказано дзвонитиfoobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

Так що все, що потрібно виконати foobarFunc (у нашому випадку anotherFunction), буде висунуте на стек. виконано, а потім забудеться про нього - цикл подій потім перейде до наступного в черзі (або прослухає повідомлення)

Ключовим тут є порядок виконання. Це є

КОЛИ щось буде працювати

Коли ви здійснюєте дзвінок за допомогою AJAX до зовнішньої сторони або запускаєте будь-який асинхронний код (наприклад, setTimeout), Javascript залежить від відповіді, перш ніж він може продовжуватися.

Велике питання - коли він отримає відповідь? Відповідь ми не знаємо - значить, цикл подій чекає цього повідомлення, щоб сказати: "Ей, біжи мене". Якщо JS просто чекав навколо цього повідомлення синхронно, то ваша програма замерзне і воно буде смоктати. Таким чином, JS продовжує виконувати наступний елемент у черзі, очікуючи, коли повідомлення повернеться до черги.

Ось чому з асинхронною функціональністю ми використовуємо речі, які називаються зворотними дзвінками . Це ніби як обіцянка досить буквально. Оскільки я обіцяю повернути щось у якийсь момент, jQuery використовує специфічні зворотні дзвінки, що називаються deffered.done deffered.failі deffered.always(серед інших). Ви можете побачити їх усіх тут

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

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

function foo(bla) {
  console.log(bla)
}

тому більшу частину часу (але не завжди) ви проходите fooне такfoo()

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


18

Використовуючи ES2017, ви повинні мати це як декларацію функції

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

І виконувати його так.

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

Або синтаксис Promise

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})

може ця друга функція повторно використовувати ??
Zum Dummi

Як ви використовуєте результати, якщо викликається oncolse, журнал? Чи все не просто переходить до консолі в цей момент?
Кен Інграм
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.