ChatGPT解决这个技术问题 Extra ChatGPT

如何从异步调用返回响应?

如何从发出异步请求的函数 foo 返回响应/结果?

我正在尝试从回调中返回值,并将结果分配给函数内部的局部变量并返回该变量,但这些方法都没有真正返回响应——它们都返回 undefined 或任何初始值变量 result 是。

接受回调的异步函数示例(使用 jQuery 的 ajax 函数):

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result; // It always returns `undefined`
}

使用 Node.js 的示例:

function foo() {
    var result;

    fs.readFile("path/to/file", function(err, data) {
        result = data;
        // return data; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}

使用承诺的 then 块的示例:

function foo() {
    var result;

    fetch(url).then(function(response) {
        result = response;
        // return response; // <- I tried that one as well
    });

    return result; // It always returns `undefined`
}
像这样使用异步 stackoverflow.com/a/47051880/2083877
@SunilKumar 我认为这没有用。 OP 提出了这个问题和自我回答,以记录如何从异步调用中获取响应。建议使用第 3 方模块违背了这样的目的,并且 IMO 该模块引入的范式不是好的做法。
@Liam:这只是接受回调的异步函数的一个示例。

N
Nick Parsons

→ 有关不同示例的异步行为的更一般解释,请参阅为什么我的变量在我在函数内部修改后没有改变? - 异步代码参考 → 如果您已经了解问题,请跳至下面可能的解决方案。

问题

Ajax 中的 A 代表 asynchronous。这意味着发送请求(或者更确切地说是接收响应)从正常的执行流程中取出。在您的示例中,$.ajax 立即返回,并且在您作为 success 回调传递的函数甚至被调用之前执行下一条语句 return result;

这是一个类比,它有望使同步流和异步流之间的区别更加清晰:

同步

想象一下,您给朋友打了个电话,请他为您查找一些东西。虽然这可能需要一段时间,但您在电话上等待并凝视空间,直到您的朋友给您所需的答案。

当您进行包含“正常”代码的函数调用时,也会发生同样的情况:

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

不是等待响应,而是立即继续执行,并执行 Ajax 调用之后的语句。为了最终获得响应,您提供了一个在收到响应后调用的函数,即回调(注意什么?回调?)。在调用回调之前执行该调用之后的任何语句。

解决方案

拥抱 JavaScript 的异步特性!虽然某些异步操作提供同步对应物(“Ajax”也是如此),但通常不鼓励使用它们,尤其是在浏览器上下文中。

你问为什么不好?

JavaScript 在浏览器的 UI 线程中运行,任何长时间运行的进程都会锁定 UI,使其无响应。此外,JavaScript 的执行时间有一个上限,浏览器会询问用户是否继续执行。

所有这些都会导致非常糟糕的用户体验。用户将无法判断一切是否正常。此外,对于连接速度较慢的用户,效果会更差。

在下文中,我们将研究三种不同的解决方案,它们都建立在彼此之上:

带有 async/await 的 Promise(ES2017+,如果您使用转译器或再生器,则在旧版浏览器中可用)

回调(在节点中流行)

then() 的承诺(ES2015+,如果您使用众多承诺库之一,则可在较旧的浏览器中使用)

所有这三个都在当前浏览器和节点 7+ 中可用。

ES2017+:异步/等待的承诺

2017 年发布的 ECMAScript 版本为异步函数引入了语法级别的支持。在 asyncawait 的帮助下,您可以以“同步风格”编写异步。代码仍然是异步的,但更容易阅读/理解。

async/await 建立在 Promise 之上:async 函数总是返回一个 Promise。 await“解包”一个promise,或者导致promise 被解析的值,或者如果promise 被拒绝则抛出一个错误。

重要提示:您只能在 async 函数或 JavaScript module 中使用 await。模块外不支持顶级 await,因此如果不使用模块,您可能必须创建异步 IIFE (Immediately Invoked Function Expression) 来启动 async 上下文。

您可以在 MDN 上阅读有关 asyncawait 的更多信息。

下面是一个详细说明上述 delay 函数 findItem() 的示例:

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

当前的 browsernode 版本支持 async/await。您还可以借助 regenerator(或使用再生器的工具,例如 Babel)将代码转换为 ES5 来支持旧环境。

让函数接受回调

回调是将函数 1 传递给函数 2。函数 2 可以在函数 1 准备就绪时调用它。在异步进程的上下文中,只要异步进程完成,就会调用回调。通常,结果被传递给回调。

在问题示例中,您可以让 foo 接受回调并将其用作 success 回调。所以这

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

变成

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

在这里,我们定义了函数“内联”,但您可以传递任何函数引用:

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) 的一个新特性,但它已经具有很好的 browser support。还有许多库实现了标准 Promises API 并提供了额外的方法来简化异步函数的使用和组合(例如,bluebird)。

Promise 是未来值的容器。当 Promise 接收到该值(已解决)或被取消(拒绝)时,它会通知所有想要访问该值的“侦听器”。

与普通回调相比,它们的优势在于它们允许您解耦代码并且它们更易于编写。

下面是一个使用 Promise 的例子:

function delay() { // `delay` 返回一个 promise return new Promise(function(resolve, reject) { // 只有 `delay` 能够解决或拒绝这个 promise setTimeout(function() { resolve(42); / / 3秒后,用值42解决promise }, 3000); }); } delay() .then(function(v) { // `delay` 返回一个 promise console.log(v); // 解析后记录值 }) .catch(function(v) { // 或者做如果它被拒绝 // (在这个例子中它不会发生,因为 `reject` 没有被调用)。 }); .as-console-wrapper { max-height: 100% !important;顶部:0; }

应用到我们的 Ajax 调用中,我们可以使用这样的 Promise:

函数 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("https://jsonplaceholder.typicode.com/todos/1") .then(function(result) { console.log(result); // 代码取决于结果 }) .catch(function() { / / 发生错误 }); .as-console-wrapper { max-height: 100% !important;顶部:0; }

描述承诺提供的所有优势超出了这个答案的范围,但如果你编写新代码,你应该认真考虑它们。它们为您的代码提供了很好的抽象和分离。

有关承诺的更多信息:HTML5 rocks - JavaScript Promises

旁注:jQuery 的延迟对象

Deferred objects 是 jQuery 的 Promise 自定义实现(在 Promise 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() 在检查服务器上的“/password”页面时不会冻结代码 - 它会向服务器发送请求,并在等待时立即返回 jQuery Ajax Deferred 对象,而不是来自服务器。这意味着 if 语句将始终获取此 Deferred 对象,将其视为 true,并像用户登录一样继续进行。不好。

但修复很简单:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

不推荐:同步“Ajax”调用

正如我所提到的,一些(!)异步操作具有同步对应物。我不提倡使用它们,但为了完整起见,以下是执行同步调用的方式:

没有 jQuery

如果您直接使用 XMLHttpRequest 对象,请将 false 作为第三个参数传递给 .open

jQuery

如果您使用 jQuery,您可以将 async 选项设置为 false。请注意,自 jQuery 1.8 起,此选项已弃用。然后,您仍然可以使用 success 回调或访问 jqXHR objectresponseText 属性:

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

如果您使用任何其他 jQuery Ajax 方法,例如 $.get$.getJSON 等,则必须将其更改为 $.ajax(因为您只能将配置参数传递给 $.ajax)。

请注意!无法发出同步 JSONP 请求。 JSONP 就其本质而言始终是异步的(甚至不考虑此选项的另一个原因)。


@Pommy:如果你想使用 jQuery,你必须包含它。请参阅docs.jquery.com/Tutorials:Getting_Started_with_jQuery
在解决方案 1,子 jQuery 中,我无法理解这一行:If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(是的,我意识到在这种情况下我的昵称有点讽刺)
@gibberish:嗯,我不知道如何使它更清楚。您是否看到如何调用 foo 并将函数传递给它 (foo(function(result) {....});)? result 在这个函数内部使用,是 Ajax 请求的响应。要引用此函数,foo 的第一个参数称为 callback 并分配给 success 而不是匿名函数。因此,$.ajax 将在请求成功时调用 callback。我试着解释一下。
这个问题的聊天已经死了,所以我不确定在哪里提出概述的更改,但我建议:1)将同步部分更改为简单讨论为什么它不好,没有代码示例说明如何做到这一点。 2)删除/合并回调示例以仅显示更灵活的延迟方法,我认为对于那些学习 Javascript 的人来说,这也可能更容易理解。
@Jessi:我认为您误解了答案的那一部分。如果您希望 Ajax 请求是同步的,则不能使用 $.getJSON。但是,您不应该希望请求是同步的,因此这不适用。您应该使用回调或承诺来处理响应,正如答案前面所述。
P
Peter Mortensen

如果您没有在代码中使用 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'

Felix Kling did a fine job 为使用 jQuery for AJAX 的人写一个答案,但我决定为不使用 jQuery 的人提供替代方案。

(Note, for those using the new fetch API, Angular or promises I've added another answer below)

你所面临的

这是另一个答案中“问题解释”的简短摘要,如果您在阅读后不确定,请阅读。

AJAX 中的 A 代表 asynchronous。这意味着发送请求(或者更确切地说是接收响应)从正常的执行流程中取出。在您的示例中,.send 立即返回,并且在您作为 success 回调传递的函数甚至被调用之前执行下一条语句 return result;

这意味着当您返回时,您定义的侦听器尚未执行,这意味着您返回的值尚未定义。

这是一个简单的类比:

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)

可能的解决方案

基本上有两种方法可以解决这个问题:

使 AJAX 调用同步(我们称之为 SJAX)。重构您的代码以与回调一起正常工作。

1. 同步 AJAX - 不要这样做!

至于同步AJAX,千万别搞! Felix 的回答引发了一些令人信服的论点,说明为什么这是一个坏主意。总而言之,它将冻结用户的浏览器,直到服务器返回响应并创建非常糟糕的用户体验。这是从 MDN 摘录的另一个简短摘要,说明了原因:

XMLHttpRequest 支持同步和异步通信。然而,一般来说,出于性能原因,异步请求应优先于同步请求。简而言之,同步请求会阻塞代码的执行……这可能会导致严重的问题……

如果您这样做,您可以传递一个标志。 Here is how

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

有关如何完成此类回调设计的更多详细信息,请查看 Felix 的答案。

现在,让我们定义 foo 本身以相应地采取行动

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // When the request is loaded
       callback(httpRequest.responseText);// We're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

(fiddle)

我们现在让我们的 foo 函数接受在 AJAX 成功完成时运行的操作。我们可以通过检查响应状态是否不是 200 并采取相应措施(创建一个失败处理程序等)来进一步扩展它。它有效地解决了我们的问题。

如果您仍然难以理解这一点,请访问 MDN read the AJAX getting started guide


“同步请求会阻塞代码的执行并可能泄漏内存和事件”同步请求如何泄漏内存?
P
Peter Mortensen

XMLHttpRequest 2(首先,阅读 Benjamin GruenbaumFelix Kling 的答案)

如果你不使用 jQuery 并且想要一个在现代浏览器和移动浏览器中工作的漂亮的简短 XMLHttpRequest 2,我建议以这种方式使用它:

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

如你看到的:

它比列出的所有其他功能都短。回调是直接设置的(因此没有额外的不必要的闭包)。它使用新的 onload(因此您不必检查 readystate && 状态) 还有一些我不记得的其他情况使 XMLHttpRequest 1 烦人。

有两种方法可以获取此 Ajax 调用的响应(三种使用 XMLHttpRequest var 名称):

最简单的:

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 advanced features

它支持所有*现代浏览器。而且我可以确认,自从 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)
}

再说一次……这是一个非常短的函数,但它会执行 GET 和 POST。

使用示例:

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

或者传递一个完整的表单元素 (document.getElementsByTagName('form')[0]):

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

或者设置一些自定义值:

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

如您所见,我没有实现同步......这是一件坏事。

话虽如此......为什么我们不做简单的方法呢?

正如评论中提到的,使用错误 && 同步确实完全打破了答案的重点。哪个是正确使用 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 设置为 blob 数组缓冲区或其他任何内容,错误处理程序可能会很有用...

即使您将 'POSTAPAPAP' 作为方法传递,它也不会引发错误。

即使您将 'fdggdgilfdghfldj' 作为 formdata 传递,它也不会引发错误。

在第一种情况下,错误位于 this.statusText 下的 displayAjax() 内,即 Method not Allowed

在第二种情况下,它可以正常工作。如果您传递了正确的发布数据,您必须在服务器端进行检查。

不允许跨域自动抛出错误。

在错误响应中,没有任何错误代码。

只有 this.type 设置为 error

如果您完全无法控制错误,为什么还要添加错误处理程序?大多数错误都在回调函数 displayAjax() 中返回。

所以:如果您能够正确复制和粘贴 URL,则不需要进行错误检查。 ;)

PS:作为第一个测试,我写了 x('x', displayAjax)...,它完全得到了响应...???所以我检查了HTML所在的文件夹,有一个名为'x.xml'的文件。因此,即使您忘记了文件的扩展名 XMLHttpRequest 2 也会找到它。我笑了

同步读取文件

不要那样做。

如果您想暂时阻止浏览器加载一个不错的大 .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 做很多事情:

我制作了一个下载管理器(使用简历、文件阅读器和文件系统两侧的范围)、使用画布的各种图像大小调整器转换器、使用 base64images 填充 Web SQL 数据库等等......

但在这些情况下,您应该只为此目的创建一个函数……有时您需要一个 blob、数组缓冲区、您可以设置标题、覆盖 mimetype 等等……

但这里的问题是如何返回 Ajax 响应......(我添加了一个简单的方法。)


虽然这个答案很好(而且我们都喜欢 XHR2 并且发布文件数据和多部分数据非常棒) - 这显示了使用 JavaScript 发布 XHR 的语法糖 - 你可能想把它放在博客文章中(我想要)甚至在图书馆中(不确定名称 xajaxxhr 可能更好:))。我看不到它如何解决从 AJAX 调用返回响应的问题。 (有人仍然可以做var res = x("url"),但不明白为什么它不起作用;))。附带说明 - 如果您从该方法返回 c 会很酷,这样用户就可以挂钩 error 等。
2.ajax is meant to be async.. so NO var res=x('url').. 这就是这个问题和答案的全部意义 :)
@cocco所以您在SO答案中编写了误导性,不可读的代码以节省一些击键?请不要那样做。
P
Peter Mortensen

如果您使用的是 Promise,那么这个答案适合您。

这意味着 AngularJS、jQuery(带延迟)、本机 XHR 的替换(提取)、Ember.jsBackbone.js 的保存或任何返回承诺的 Node.js 库。

你的代码应该是这样的:

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.

Felix Kling did a fine job 为使用带有 Ajax 回调的 jQuery 的人编写答案。我有一个原生 XHR 的答案。这个答案适用于前端或后端的 Promise 的通用用法。

核心问题

浏览器和服务器上的 JavaScript 并发模型与 Node.js/io.js 是异步和反应式的。

每当您调用返回 Promise 的方法时,then 处理程序总是 异步执行 - 即,它们下方不在 .then 中的代码之后处理程序。

这意味着当您返回 data 时,您定义的 then 处理程序尚未执行。这反过来意味着您返回的值没有及时设置为正确的值。

这是这个问题的一个简单类比:

函数 getFive(){ var 数据; setTimeout(function(){ // 设置未来一秒的定时器 data = 5; // 一秒后,执行此操作 }, 1000);返回数据; } document.body.innerHTML = getFive(); // 这里是 `undefined` 而不是 5

data 的值为 undefined,因为 data = 5 部分尚未执行。它可能会在一秒钟内执行,但到那时它与返回值无关。

由于操作尚未发生(Ajax、服务器调用、I/O 和计时器),您在请求有机会告诉您的代码该值是什么之前返回该值。

这个问题的一种可能的解决方案是重新编写代码,告诉你的程序在计算完成后要做什么。 Promise 通过本质上是临时的(时间敏感的)来积极地实现这一点。

快速回顾承诺

Promise 是一个随时间变化的值。 Promise 有状态。它们以没有价值的待处理开始,并且可以解决:

完成意味着计算成功完成。

拒绝意味着计算失败。

一个promise 只能改变状态一次,之后它将永远保持在同一个状态。您可以将 then 处理程序附加到 Promise 以提取它们的值并处理错误。 then 处理程序允许 chaining 次调用。 Promise 由 using APIs that return them 创建。例如,更现代的 Ajax 替换 fetch 或 jQuery 的 $.get 返回承诺。

当我们在一个 Promise 上调用 .then 并从中返回一些东西时 - 我们得到一个 已处理值 的 Promise。如果我们兑现另一个承诺,我们会得到惊人的东西,但让我们抓住我们的马。

带着承诺

让我们看看如何用 Promise 解决上述问题。首先,让我们通过使用 Promise constructor 创建延迟函数来展示我们对上述承诺状态的理解:

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

现在,在我们 converted setTimeout 使用 Promise 之后,我们可以使用 then 使其计数:

function delay(ms){ // 花费毫秒数 // 返回一个新的 promise return new Promise(function(resolve, reject){ setTimeout(function(){ // 时间到了,resolve(); // 改变对已完成状态的承诺 }, ms); }); } function getFive(){ // 我们正在返回承诺。请记住,promise 是我们的值的包装器 return delay(100).then(function(){ // 当 promise 准备好时,返回 5; // 返回值 5。Promise 都是关于返回值 }) } / / 我们_必须_在调用站点中像这样包装它,我们无法访问纯值 getFive().then(function(five){ document.body.innerHTML = 五; });

基本上,我们不是返回一个由于并发模型而无法执行的 value - 我们返回一个 wrapper 以获取我们可以 unwrap< /em> 与 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`
})

所以这同样有效。我们已经知道我们不能从已经异步的调用中返回值,但是我们可以使用 Promise 并链接它们来执行处理。我们现在知道如何从异步调用返回响应。

ES2015 (ES6)

ES6 引入了 generators ,它们是可以在中间返回然后恢复它们所在点的函数。这通常对序列很有用,例如:

function* foo(){ // Notice the star. This is ES6, so new browsers, Nodes.js, and io.js only
    yield 1;
    yield 2;
    while(true) yield 3;
}

是一个函数,它在可以迭代的序列 1,2,3,3,3,3,.... 上返回一个 iterator。虽然这本身很有趣并且为很多可能性打开了空间,但有一个特别有趣的案例。

如果我们生成的序列是一系列动作而不是数字 - 我们可以在产生动作时暂停函数并在恢复函数之前等待它。因此,我们需要一系列未来值,而不是数字序列——即:promise。

这有点棘手,但非常强大的技巧让我们以同步的方式编写异步代码。有几个“跑步者”可以为你做这件事。写一个是短短的几行代码,但这超出了这个答案的范围。我将在这里使用 Bluebird 的 Promise.coroutine,但还有其他包装器,例如 coQ.async

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

此方法本身返回一个 Promise,我们可以从其他协程中使用它。例如:

var main = coroutine(function*(){
   var bar = yield foo(); // Wait our earlier coroutine. It returns a promise
   // The server call is done here, and the 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 are done
});
main();

ES2016 (ES7)

在 ES7 中,这被进一步标准化。目前有几个提案,但您可以在所有提案中await承诺。通过添加 asyncawait 关键字,这只是上面 ES6 提案的“糖”(更好的语法)。制作上面的例子:

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
}

它仍然返回一个相同的承诺:)


P
Peter Mortensen

您错误地使用了 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);
    }
});

在提交处理程序中返回任何内容都不会做任何事情。相反,你必须要么交出数据,要么直接在成功函数中做你想做的事。


这个答案是完全语义的......你的成功方法只是回调中的回调。您可以只拥有 success: handleData 并且它会起作用。
J
Johannes Fahrenkrug

我会用一个看起来很可怕的手绘漫画来回答。第二张图片是您的代码示例中 resultundefined 的原因。

https://i.imgur.com/v5ksbBC.jpg


一张图片值一千字,A 人 - 询问 B 人的详细信息来修理他的车,然后人 B - 进行 Ajax 调用并等待服务器的响应以获取汽车修理细节,当收到响应时,Ajax Success 函数调用此人B 函数并将响应作为参数传递给它,人 A 接收答案。
如果您在每个图像中添加代码行来说明概念,那就太好了。
与此同时,开车的人被困在路边。他要求汽车修好后再继续。他现在一个人在路边等着……他宁愿打电话等状态变化,但机械师不会这样做……机械师说他必须继续工作,不能简单地挂电话。机械师保证会尽快给他回电话。大约 4 小时后,这个人放弃了,打电话给优步。 - 超时示例。
但是使用回调函数,我觉得最后一帧左边的人被迫不给对方他们的电话号码。相反,他们必须告诉对方,“这就是我想用电话里那个家伙的信息做的所有事情。做所有这些事情,永远不要告诉我。”我错过了什么?
@FingLixon 无论如何这都不是一部完美的漫画:-D。第二张图片应该说明当您尝试过早(在回调发生之前)读取值时会发生什么。第三张图片说明了设置回调方法:左边的人基本上是回调处理程序:一旦信息可用,他将被调用,然后可以随心所欲地使用它。我现在认为在这部漫画中打两个电话是个坏主意:打给商店的电话和打给左边那个人的电话。我应该简化一下,对此感到抱歉。
P
Peter Mortensen

最简单的解决方案是创建一个 JavaScript 函数并为 Ajax success 回调调用它。

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

我不知道是谁投了反对票。但这是一项有效的工作,实际上我使用这种方法来创建整个应用程序。 jquery.ajax 不返回数据,因此最好使用上述方法。如果它是错误的,那么请解释并提出更好的方法来做到这一点。
对不起,我忘了发表评论(我通常这样做!)。我投了反对票。否决票并不表示事实正确或缺乏,它们表示在上下文中的有用性或缺乏。鉴于 Felix 已经更详细地解释了这一点,我认为您的答案没有用。附带说明一下,如果响应是 JSON,为什么要对响应进行字符串化?
好的.. @Benjamin 我使用 stringify 将 JSON 对象转换为字符串。并感谢您澄清您的观点。将记住发布更详细的答案。
P
Peter Mortensen

角 1

使用 AngularJS 的人可以使用 promises 处理这种情况。

Here 它说,

Promise 可用于取消嵌套异步函数,并允许将多个函数链接在一起。

您还可以找到一个很好的解释 here

下面提到的 documentation 中的示例。

  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.

Angular 2 及更高版本

在 Angular 2 中查看以下示例,但它的 recommendedobservables 与 Angular 2 一起使用。

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

请参阅此处的 original 帖子。但是 TypeScript 不支持 native ES6 Promises,如果你想使用它,你可能需要插件。

此外,这里是 promises specification


不过,这并没有解释 Promise 是如何解决这个问题的。
jQuery 和 fetch 方法也都返回 Promise。我建议修改你的答案。虽然 jQuery 的不太一样(然后在那里,但 catch 不是)。
H
Henke

这里的大多数答案都为您何时进行单个异步操作提供了有用的建议,但有时,当您需要对数组或其他类似列表的结构中的每个条目执行异步操作时,就会出现这种情况。这样做的诱惑是:

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

例子:

// 错误 var theArray = [1, 2, 3];变种结果 = []; theArray.forEach(function(entry) { doSomethingAsync(entry, function(result) { results.push(result); }); }); console.log("结果:", 结果); // 例如,使用它们,返回它们等等 function doSomethingAsync(value, callback) { console.log("Starting async operation for " + value); setTimeout(function() { console.log("完成异步操作" + value); callback(value * 2); }, Math.floor(Math.random() * 200)); } .as-console-wrapper { max-height: 100% !important; }

不起作用的原因是,当您尝试使用结果时,来自 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
        }
    });
});

例子:

var theArray = [1, 2, 3];变种结果 = []; var 期待 = theArray.length; theArray.forEach(function(entry, index) { doSomethingAsync(entry, function(result) { results[index] = result; if (--expecting === 0) { // 完成!console.log("Results:" , JSON.stringify(results)); // 例如,使用结果 } }); }); function doSomethingAsync(value, callback) { console.log("开始异步操作 " + value); setTimeout(function() { console.log("完成异步操作" + value); callback(value * 2); }, Math.floor(Math.random() * 200)); } .as-console-wrapper { max-height: 100% !important; }

(我们可以取消 expecting 而只使用 results.length === theArray.length,但这让我们有可能在调用未完成时更改 theArray...)

请注意我们如何使用 forEach 中的 index 将结果保存在 results 中与其相关条目相同的位置,即使结果无序到达(因为异步调用不一定在它们开始的顺序)。

但是,如果您需要从函数中返回这些结果怎么办?正如其他答案所指出的那样,您不能;您必须让您的函数接受并调用回调(或返回 Promise)。这是一个回调版本:

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

例子:

函数 doSomethingWith(theArray, callback) { var results = []; var 期待 = theArray.length; theArray.forEach(function(entry, index) { doSomethingAsync(entry, function(result) { results[index] = result; if (--expecting === 0) { // 完成!回调(results); } }) ; }); } doSomethingWith([1, 2, 3], function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value, callback) { console.log("开始异步操作 " + value); setTimeout(function() { console.log("完成异步操作" + value); callback(value * 2); }, Math.floor(Math.random() * 200)); } .as-console-wrapper { max-height: 100% !important; }

或者这是一个返回 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 拒绝承诺。)

例子:

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) { // 完成!resolve(results); } }); }); }); } doSomethingWith([1, 2, 3]).then(function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value, callback) { console.log("开始异步操作 " + value); setTimeout(function() { console.log("完成异步操作" + value); callback(value * 2); }, Math.floor(Math.random() * 200)); } .as-console-wrapper { max-height: 100% !important; }

(或者,您可以为 doSomethingAsync 创建一个返回承诺的包装器,然后执行以下操作...)

如果 doSomethingAsync 为您提供 Promise,您可以使用 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 将忽略第二个和第三个参数,您可以直接将其传递给 mapmap 使用三个参数调用它的回调,但大多数人大部分时间只使用第一个):

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

例子:

函数 doSomethingWith(theArray) { return Promise.all(theArray.map(doSomethingAsync)); } doSomethingWith([1, 2, 3]).then(function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value) { console.log("开始异步操作 " + value); return new Promise(function(resolve) { setTimeout(function() { console.log("完成异步操作" + value); resolve(value * 2); }, Math.floor(Math.random() * 200) ); }); } .as-console-wrapper { max-height: 100% !important; }

请注意,当您给它的所有承诺都被解决时,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;,但在某些以下示例我们没有要使用的索引。)

例子:

函数 doSomethingWith(theArray, callback) { var results = [];做一个(0); function doOne(index) { if (index < theArray.length) { doSomethingAsync(theArray[index], function(result) { results.push(result); doOne(index + 1); }); } else { // 完成!回调(结果); } } } doSomethingWith([1, 2, 3], function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value, callback) { console.log("开始异步操作 " + value); setTimeout(function() { console.log("完成异步操作" + value); callback(value * 2); }, Math.floor(Math.random() * 200)); } .as-console-wrapper { max-height: 100% !important; }

(或者,再次为 doSomethingAsync 构建一个包装器,为您提供承诺并执行以下操作...)

如果 doSomethingAsync 给您一个 Promise,如果您可以使用 ES2017+ 语法(可能使用像 Babel 这样的转译器),您可以将 async functionfor-ofawait 一起使用:

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

例子:

异步函数 doSomethingWith(theArray) { const 结果 = []; for (const entry of theArray) { results.push(await doSomethingAsync(entry)); } 返回结果; } doSomethingWith([1, 2, 3]).then(function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value) { console.log("开始异步操作 " + value); return new Promise(function(resolve) { setTimeout(function() { console.log("完成异步操作" + value); resolve(value * 2); }, Math.floor(Math.random() * 200) ); }); } .as-console-wrapper { max-height: 100% !important; }

如果你还不能使用 ES2017+ 语法,你可以使用 "Promise reduce" pattern 的变体(这比通常的 Promise reduce 更复杂,因为我们不会将结果从一个传递到下一个,而是收集他们的结果在一个数组中):

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

例子:

function doSomethingWith(theArray) { return theArray.reduce(function(p, entry) { return p.then(function(results) { return doSomethingAsync(entry).then(function(result) { results.push(result); 返回结果; }); }); }, Promise.resolve([])); } doSomethingWith([1, 2, 3]).then(function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value) { console.log("开始异步操作 " + value); return new Promise(function(resolve) { setTimeout(function() { console.log("完成异步操作" + value); resolve(value * 2); }, Math.floor(Math.random() * 200) ); }); } .as-console-wrapper { max-height: 100% !important; }

...使用 ES2015+ arrow functions 不太麻烦:

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

例子:

function doSomethingWith(theArray) { return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => { results.push(result); return results; })), Promise.resolve([])); } doSomethingWith([1, 2, 3]).then(function(results) { console.log("Results:", JSON.stringify(results)); }); function doSomethingAsync(value) { console.log("开始异步操作 " + value); return new Promise(function(resolve) { setTimeout(function() { console.log("完成异步操作" + value); resolve(value * 2); }, Math.floor(Math.random() * 200) ); }); } .as-console-wrapper { max-height: 100% !important; }


您能解释一下代码的 if (--expecting === 0) 部分是如何工作的吗?您的解决方案的回调版本对我来说非常有用,我只是不明白您如何使用该语句检查已完成的响应数量。感谢这只是我缺乏知识。有没有另一种方法可以写支票?
@Sarah:expectingarray.length 的值开始,即我们要发出多少请求。我们知道在所有这些请求开始之前不会调用回调。在回调中,if (--expecting === 0) 执行以下操作: 1. 递减 expecting(我们已收到响应,因此我们预计会少一个响应)并且如果值 after 减量为 0(我们不再期待任何回应),我们完成了!
@Henke - 我认为这确实是个人喜好,虽然通常我更喜欢记录原始数据并让控制台处理它,但在这种特定情况下,我认为你对更改是正确的。谢谢! :-)
为了我自己(和其他人?)的方便,添加指向相关答案的链接:How to make many asynchronous calls and wait for them all
F
Francisco Carmona

看看这个例子:

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 返回一个 已解决的 promise(返回 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);
  });
})();

P
Peter Mortensen

这是在许多新的 JavaScript 框架中使用的双向数据绑定或存储概念对您非常有用的地方之一......

因此,如果您使用 AngularReact 或任何其他执行双向数据绑定或存储概念的框架,则此问题已为您解决,简单来说,您的结果首先是 undefined阶段,因此您在收到数据之前已经获得了 result = undefined,然后一旦您获得结果,它将被更新并分配给您的 Ajax 调用响应的新值...

但是,例如,正如您在这个问题中所问的那样,您如何在纯 JavaScript 或 jQuery 中做到这一点?

你可以使用回调、promise 和最近 observable 来为你处理它。例如,在 Promise 中,我们有一些像 success()then() 这样的函数,它们将在您的数据准备好时执行。与可观察对象上的回调或 subscribe 函数相同。

例如,在您使用 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(); // The call happens here
});

有关更多信息,请研究 promises 和 observables,它们是执行此异步操作的新方法。


这在全局范围内很好,但在某些模块上下文中,您可能希望确保回调的正确上下文,例如 $.ajax({url: "api/data", success: fooDone.bind(this)});
这实际上是不正确的,因为 React 是单向数据绑定
@MatthewBrent 你没有错,但也不对,React 道具是对象,如果更改,它们会在整个应用程序中更改,但这不是 React 开发人员推荐使用它的方式...
P
Peter Mortensen

这是我们在与 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),但它确实启动了一个计时器,该计时器在 1 秒后执行一个函数以返回“哇”。但是正如你所看到的,分配给 bar 的值是从 foo() 立即返回的东西,它什么都不是,即只是 undefined

那么,我们该如何解决这个问题呢?

让我们向我们的函数请求一个承诺。 Promise 的真正含义是:它意味着该函数保证您提供将来获得的任何输出。所以让我们看看它在上面的小问题中的作用:

function foo(){
   return new Promise((resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){
      // Promise is RESOLVED, when the 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 处理 Promise 之外,还有另一种方法。这个想法是识别一个异步函数,然后等待承诺解决,然后再转到下一行代码。它仍然只是引擎盖下的 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);
     }
  }

这仍然被认为是从 promise 或 async/await 返回值的最佳方式吗?
@edwardsmarkf 我个人认为没有最好的方法。我将 promise 与 then/catch 、 async/await 以及代码的异步部分的生成器一起使用。这在很大程度上取决于使用环境。
P
Peter Mortensen

从异步函数返回值的另一种方法是传入一个对象,该对象将存储异步函数的结果。

这是一个相同的例子:

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 对象来存储值。即使在异步作业之后,这也允许结果可用。

我经常使用这种方法。我很想知道这种方法在涉及通过连续模块将结果连接回的情况下效果如何。


在这里使用对象并没有什么特别之处。如果您将他的响应直接分配给 result,它也会起作用。它之所以有效,是因为您在异步函数完成之后读取变量
r
rohithpr

虽然 Promise 和回调在许多情况下都可以正常工作,但在后面表达类似这样的内容是很痛苦的:

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
}

您可以签出项目 here


这仍然相关吗?
如果您使用一些最新版本的节点,则可以使用 async-await。如果有人坚持使用旧版本,他们可以使用此方法。
l
loretoparisi

我编写的以下示例显示了如何

处理异步 HTTP 调用;

等待每个 API 调用的响应;

使用承诺模式;

使用 Promise.all 模式加入多个 HTTP 调用;

这个工作示例是独立的。它将定义一个简单的请求对象,该对象使用窗口 XMLHttpRequest 对象进行调用。它将定义一个简单的函数来等待一堆承诺完成。

语境。该示例查询 Spotify Web API 端点以搜索给定查询字符串集的 playlist 对象:

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

对于每个项目,一个新的 Promise 将触发一个块 - ExecutionBlock,解析结果,根据结果数组安排一组新的 Promise,即 Spotify user 对象的列表,并在ExecutionProfileBlock 异步。

然后,您可以看到一个嵌套的 Promise 结构,它允许您生成多个完全异步的嵌套 HTTP 调用,并通过 Promise.all 连接每个调用子集的结果。

注意 最近的 Spotify search API 需要在请求标头中指定访问令牌:

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

因此,要运行以下示例,您需要将访问令牌放入请求标头中:

var spotifyAccessToken = "YourSpotifyAccessToken"; var console = { log: function(s) { document.getElementById("console").innerHTML += s + "
" } } // 简单 XMLHttpRequest // 基于 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 尝试 { request = new ActiveXObject('Msxml2.XMLHTTP'); } catch (e) { try { request = new ActiveXObject('Microsoft.XMLHTTP'); } catch (e) {} } } // 状态改变 request.onreadystatechange = function() { if (request.readyState === 4) { // Done if (request.status === 200) { // 完成响应(request.responseText) } 否则响应(); } } request.open('GET', 什么, true); request.setRequestHeader("授权", "承载" + spotifyAccessToken); request.send(null); } } //PromiseAll var promiseAll = function(items, block, done, fail) { var self = this; var 承诺 = [],索引 = 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:延迟执行块 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("发出请求 [" + index + "]") ExecutionBlock(item, index, resolve, reject); }, function(results) { // 汇总结果 console.log("收到的所有配置文件 " + results.length); //console.log(JSON.stringify(results[0], null, 2)); ///// promiseall 再次 var ExecutionProfileBlock = function(item, index, resolve, reject) { SimpleRequest.call(item, function(result) { if (result) { var obj = JSON.parse(result); resolve({ name: obj.display_name , follower: obj.followers.total, url: obj.href }); } //result }) } //ExecutionProfileBlock promiseAll(results[0], function(item, index, resolve, reject) { //console.log("发出请求 [" + index + "] " + item) ExecutionProfileBlock(item, index, resolve, reject); }, function(results) { // 汇总结果 console.log("All response received " + results.length); console.log(JSON.stringify(results, null, 2)); } , function(error) { // 错误 console.log(error); }) ///// }, function(error) { // 错误 console.log(error); });

我已经广泛讨论了这个解决方案 here


P
Peter Mortensen

简短的回答是,您必须实现这样的回调:

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

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

4
404

JavaScript 是单线程的。

浏览器可以分为三个部分:

事件循环 Web API 事件队列

事件循环永远运行,即一种无限循环。事件队列是所有函数都被推送到某个事件(例如:点击)的地方。

这是从队列中逐一执行并放入事件循环中,该循环执行此函数并在执行第一个函数后为下一个函数做好准备。这意味着一个函数的执行直到队列中它之前的函数在事件循环中执行后才开始。

现在让我们认为我们将两个函数推送到一个队列中。一种是从服务器获取数据,另一种是利用该数据。我们先将 serverRequest() 函数推送到队列中,然后再推送 utiliseData() 函数。 serverRequest 函数进入事件循环并调用服务器,因为我们永远不知道从服务器获取数据需要多少时间,所以这个过程预计需要时间,所以我们忙于我们的事件循环从而挂起我们的页面。

这就是 Web API 发挥作用的地方。它从事件循环中获取这个函数并处理服务器使事件循环空闲,以便我们可以执行队列中的下一个函数。

队列中的下一个函数是 utiliseData() ,它进入循环,但由于没有可用的数据,它会被浪费,并且下一个函数的执行会一直持续到队列结束。 (这称为异步调用,即,我们可以做其他事情,直到我们得到数据。)

让我们假设我们的 serverRequest() 函数在代码中有一个 return 语句。当我们从服务器 Web API 取回数据时,它会将数据推送到队列末尾的队列中。

当它在队列末尾被推送时,我们无法利用它的数据,因为我们的队列中没有任何函数可以利用这些数据。因此不可能从异步调用中返回一些东西。

因此,解决方案是回调或承诺。

来自此处答案之一的图像正确解释了回调的使用... *

我们将我们的函数(利用从服务器返回的数据的函数)提供给调用服务器的函数。

https://i.stack.imgur.com/UCJgN.png

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

JavaScript.info callback


P
Peter Mortensen

2017 年回答:您现在可以在每个当前浏览器和 Node.js 中执行您想要的操作

这很简单:

返回一个承诺

使用 'await',它会告诉 JavaScript 等待将 Promise 解析为一个值(如 HTTP 响应)

将“异步”关键字添加到父函数

这是您的代码的工作版本:

(async function(){

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

})()

await is supported in all current browsers and Node.js 8


不幸的是,这仅适用于返回承诺的函数——例如,它不适用于使用回调的 Node.js API。我不建议在没有 Babel 的情况下使用它,因为不是每个人都使用“当前浏览器”。
@MichałPerłakowski 节点 8 包含 nodejs.org/api/util.html#util_util_promisify_original 可用于使 node.js API 返回承诺。您是否有时间和金钱来支持非当前浏览器显然取决于您的情况。
IE 11 在 2018 年仍然是当前的浏览器,遗憾的是它不支持 await/async
IE11 不是当前的浏览器。它于 5 年前发布,根据 caniuse 的数据,其全球市场份额为 2.5%,除非有人将您的预算翻倍以忽略所有当前的技术,否则它不值得大多数人花时间。
P
Peter Mortensen

您可以使用这个自定义库(使用 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)
});

P
Peter Mortensen

另一种解决方案是通过顺序执行器 nsynjs 执行代码。

如果底层功能被承诺

nsynjs 将依次评估所有的 Promise,并将 Promise 结果放入 data 属性中:

函数 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('接收到的字节数:',getURL(url).length); }; nsynjs.run(synchronousCode,{},function(){ console.log('synchronousCode done'); });

如果底层函数没有被承诺

步骤 1. 将带有回调的函数包装到 nsynjs-aware 包装器中(如果它有 promisified 版本,则可以跳过此步骤):

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

Step 3. 通过nsynjs同步运行函数:

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

Nsynjs 将逐步评估所有运算符和表达式,如果某些慢速函数的结果未准备好,则暂停执行。

更多示例是 here


这是有趣的。我喜欢它允许以其他语言编写异步调用的方式。但从技术上讲,它不是真正的 JavaScript?
J
James

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 包装在一个数组中,并在您读取它们时进行解构,以便该模式可以处理具有多个参数的回调。例如 node fs

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

你认为生成器/异步生成器是单独的异步 API 解决方案吗?或者你会使用生成器来包装另一个异步 API,比如 promise/defered ?我同意这是对异步世界的另一个强大补充,但仍然没有找到让我采用它们的生成器的正确用法。
5
5 revs, 4 users 87% user663031

我们发现自己处于一个似乎沿着我们称为“时间”的维度前进的宇宙中。我们并不真正了解时间是什么,但我们已经开发了抽象和词汇,让我们可以推理和谈论它:“过去”、“现在”、“未来”、“之前”、“之后”。

我们构建的计算机系统——越来越多——将时间作为一个重要维度。某些事情注定要在未来发生。然后其他事情需要在这些第一件事最终发生之后发生。这就是所谓的“异步”的基本概念。在我们日益网络化的世界中,最常见的异步情况是等待某个远程系统响应某个请求。

考虑一个例子。你打电话给送奶工,要一些牛奶。当它出现时,你想把它放在你的咖啡里。你现在不能把牛奶放进你的咖啡里,因为它还没有出现。您必须等待它来,然后才能将其放入咖啡中。换句话说,以下内容不起作用:

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

因为 JavaScript 无法知道在执行 put_in_coffee 之前它需要 等待 order_milk 完成。换句话说,它不知道 order_milk异步的——直到未来某个时间才会产生牛奶。 JavaScript 和其他声明性语言在不等待的情况下一个接一个地执行语句。

解决这个问题的经典 JavaScript 方法,利用 JavaScript 支持函数作为可以传递的第一类对象这一事实,是将函数作为参数传递给异步请求,然后在完成时调用它它的任务在未来的某个时候。这就是“回调”方法。它看起来像这样:

order_milk(put_in_coffee);

order_milk 开始,订购牛奶,然后,当且仅当它到达时,它才会调用 put_in_coffee

这种回调方法的问题在于它污染了函数的正常语义,该函数使用 return 报告其结果;相反,函数不得通过调用作为参数给出的回调来报告其结果。此外,在处理较长的事件序列时,这种方法可能会迅速变得笨拙。例如,假设我想等待牛奶放入咖啡中,然后才执行第三步,即喝咖啡。我最终需要写这样的东西:

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

我将要放入其中的牛奶以及放入牛奶后执行的操作 (drink_coffee) 传递给 put_in_coffee。这样的代码变得难以编写、阅读和调试。

在这种情况下,我们可以将问题中的代码重写为:

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

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

输入承诺

这就是“承诺”概念的动机,它是一种特殊类型的值,代表某种未来异步结果。它可以代表已经发生的事情,或者将来会发生的事情,或者可能永远不会发生。 Promise 有一个名为 then 的方法,当 Promise 所代表的结果实现时,您将向该方法传递要执行的操作。

对于我们的牛奶和咖啡,我们设计 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');
}

实际上,我们所做的只是在对 $.ajax 的调用中添加了一个 return。这是因为 jQuery 的 $.ajax 已经返回了一种类似于 Promise 的东西。 (实际上,在不深入细节的情况下,我们更愿意包装这个调用以便返回一个真正的 Promise,或者使用 $.ajax 的替代方法。)现在,如果我们想要加载文件并等待它完成然后做某事,我们可以简单地说

get_data() . then(do_something)

例如,

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

在使用 Promise 时,我们最终会将大量函数传递给 then,因此使用更紧凑的 ES6 风格的箭头函数通常会有所帮助:

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

异步关键字

但是,如果是同步的,必须以一种方式编写代码,而如果是异步的,则必须以一种完全不同的方式编写代码,这仍然有些令人不满意的地方。对于同步,我们写

a();
b();

但是如果 a 是异步的,那么我们必须编写 promise

a() . then(b);

上面,我们说过,“JavaScript 无法知道它需要等待第一个调用完成,然后再执行第二个”。如果有某种方法可以告诉 JavaScript,那不是很好吗?事实证明,有 - await 关键字,用于一种称为“异步”函数的特殊类型的函数中。此功能是即将发布的 ECMAScript (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);
}

D
David R Tribble

简短回答:您的 foo() 方法立即返回,而 $ajax() 调用在函数返回后异步执行。然后问题是如何或在哪里存储异步调用返回后检索到的结果。

在这个线程中已经给出了几个解决方案。也许最简单的方法是将对象传递给 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 中。


虽然这可行,但它并不比分配给全局变量更好。
C
Community

以下是一些处理异步请求的方法:

Browser Promise object Q - JavaScript A+ Promises.js jQuery 延迟 XMLHttpRequest API 的 promise 库使用回调概念 - 作为第一个答案中的实现

示例:jQuery 延迟实现以处理多个请求

var 应用程序 = 应用程序 || {}; 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); });延期退货; }, init: function(){ this.getDataFromServer().done(_.bind(function(resp1, resp2) { // 当你得到 Ajax 的响应时,做你想做的操作,例如,记录响应。}, this)); } };应用程序.init();


为什么要包含输出错误的堆栈片段?
P
Peter Mortensen

foo() 成功中使用 callback() 函数。用这种方法试试。它简单易懂。

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

H
Henke

1. 第一个绊脚石

对于其他许多人来说,我遇到的异步调用一开始是令人费解的。我不记得细节,但我可能尝试过类似的东西:

让结果; $.ajax({ url: 'https://jsonplaceholder.typicode.com/todos/1', 成功: function (response) { console.log('\nInside $.ajax:'); console.log(response) ; 结果 = 响应; } }); console.log('最后,结果:' + 结果); .as-console-wrapper { max-height: 100% !important;顶部:0; }

哎呀! 我认为会打印last 的行 console.log('Finally, the result: ' + result); 的输出实际上是在 另一个输出之前打印的! – 它不包含结果:它只打印 undefined1 怎么会?

有用的见解

我清楚地记得我第一次啊哈!关于如何理解异步调用的时刻。
它是this comment说:
你实际上不想获取数据退出回调;
您希望将需要数据的操作进入回调!
2
这是在上面的示例中很明显。
但是是否仍然可以编写代码异步调用完成后处理响应?

2. 纯 JavaScript 和回调函数

答案是肯定的! - 有可能的。一种替代方法是在延续传递样式中使用回调函数:3

const url = 'https://jsonplaceholder.typicode.com/todos/2';函数 asynchronousCall (callback) { const request = new XMLHttpRequest(); request.open('GET', url);请求.发送(); request.onload = function () { if (request.readyState === request.DONE) { console.log('请求完成。现在回调。');回调(request.responseText); } }; } asynchronousCall(function (result) { console.log('这是回调函数的开始。结果:'); console.log(result); console.log('回调函数在这一行结束。结束! '); }); console.log('代码中的最后一个,但首先执行!'); .as-console-wrapper { max-height: 100% !important;顶部:0; }

请注意函数 asynchronousCallvoid。它什么也不返回。相反,通过使用匿名回调函数 (asynchronousCall(function (result) {...) 调用 asynchronousCall,此函数对结果执行所需的操作,但仅在请求完成后 - 当 responseText 可用时.

运行上面的代码片段显示了我可能不想在异步调用之后编写任何代码(例如行 LAST in the code, but executed FIRST!)。
为什么? - 因为此类代码将在异步调用传递任何响应数据之前发生。
在比较代码输出。

3. 使用 .then() 或 async/await 承诺

.then() 构造在 2015 年 6 月的 ECMA-262 第 6 版中引入,而 async/await 构造在 2017 年 6 月的 ECMA-262 第 8 版中引入.
下面的代码仍然是纯 JavaScript,将老式的 XMLHttpRequest 替换为 Fetch4

fetch('http://api.icndb.com/jokes/random') .then(response => response.json()) .then(responseBody => { console.log('.then() - 响应正文:'); console.log(JSON.stringify(responseBody) + '\n\n'); });异步函数 receiveAndAwaitPromise () { const responseBody = (await fetch('http://api.icndb.com/jokes/random')).json(); console.log('async/await:'); console.log(JSON.stringify(await responseBody) + '\n\n'); } receiveAndAwaitPromise(); .as-console-wrapper { max-height: 100% !important;顶部:0; }

如果您决定使用 async/await 构造,有必要提出警告。请注意,在上面的代码段中,两个位置需要await。如果一开始就忘记了,就没有输出。如果第二次忘记了,唯一的输出将是空对象,{}(或 [object Object][object Promise])。
忘记函数的 async 前缀可能是最糟糕的——输出将是 "SyntaxError: missing ) in parenthetical" - 没有提及 missing async 关键字。

4. Promise.all – URL 数组 5

假设我们需要请求一大堆 URL。我可以发送一个请求,等待它响应,然后发送下一个请求,等待它响应,依此类推……啊! – 这可能需要很长时间。如果我可以一次发送它们,然后等待最慢响应到达所花费的时间不是更好吗?

作为一个简化的例子,我将使用:

urls = ['https://jsonplaceholder.typicode.com/todos/2',
        'https://jsonplaceholder.typicode.com/todos/3']

两个 URL 的 JSON:

{"userId":1,"id":2,"title":"quis ut nam facilis et officia qui",
 "completed":false}
{"userId":1,"id":3,"title":"fugiat veniam minus","completed":false}

目标是获取对象数组,其中每个对象都包含来自相应 URL 的 title 值。

为了让它更有趣一点,我假设已经有一个名称数组,我希望 URL 结果数组(标题)与之合并:

namesonly = ['two', 'three']

所需的输出是将 namesonlyurls 组合成 对象数组 的混搭:

[{"name":"two","loremipsum":"quis ut nam facilis et officia qui"},
{"name":"three","loremipsum":"fugiat veniam minus"}]

我已将 title 的名称更改为 loremipsum

const namesonly = ['two','three']; const urls = ['https://jsonplaceholder.typicode.com/todos/2', 'https://jsonplaceholder.typicode.com/todos/3']; Promise.all(urls.map(url => fetch(url) .then(response => response.json()) .then(responseBody => responseBody.title))) .then(titles => { const names = namesonly .map(value => ({ name: value })); console.log('names: ' + JSON.stringify(names)); const latins = title.map(value => ({ loremipsum: value })) ; console.log('latins:\n' + JSON.stringify(latins)); const result = names.map((item, i) => Object.assign({}, item, latins[i])); console.log('result:\n' + JSON.stringify(result)); }); .as-console-wrapper { max-height: 100% !important;顶部:0; }

上述所有示例都很简短,简洁地传达了异步调用如何用于玩具 API。使用小型 API 可以很好地解释概念和工作代码,但这些示例可能有点枯燥。

下一节将展示一个更现实的示例,说明如何组合 API 以创建更有趣的输出。

5. 如何在 Postman 6 中可视化混搭

The MusicBrainz API 包含有关艺术家和乐队的信息。
一个示例 - 对英国摇滚乐队 Coldplay 的请求是:
http://musicbrainz.org/ws/2/artist/cc197bad-dc9c-440d-a5b5-d52ba2e14234?&fmt=json&inc=url-rels+release-groups
JSON 响应包含 - 其中其他东西——乐队最早的 25 张专辑名称。此信息位于 release-groups 数组中。这个数组的开始,包括它的第一个对象是:

...
  "release-groups": [
    {
      "id": "1dc4c347-a1db-32aa-b14f-bc9cc507b843",
      "secondary-type-ids": [],
      "first-release-date": "2000-07-10",
      "primary-type-id": "f529b476-6e62-324f-b0aa-1f3e33d313fc",
      "disambiguation": "",
      "secondary-types": [],
      "title": "Parachutes",
      "primary-type": "Album"
    },
...

这个 JSON 片段显示 Coldplay 的第一张专辑是 Parachutes。它还给出了一个 id,在本例中为 1dc4c347-a1db-32aa-b14f-bc9cc507b843,它是专辑的唯一标识符。

此标识符可用于在 the Cover Art Archive API:
http://coverartarchive.org/release-group/1dc4c347-a1db-32aa-b14f-bc9cc507b843 中进行查找。 7

对于每张专辑,JSON 响应都包含一些图像,其中一张是专辑的封面。对上述请求的响应的前几行:

{
  "images": [
    {
      "approved": true,
      "back": false,
      "comment": "",
      "edit": 22132705,
      "front": true,
      "id": 4086974851,
      "image": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851.jpg",
      "thumbnails": {
        "250": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-250.jpg",
        "500": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-500.jpg",
        "1200": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-1200.jpg",
        "large": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-500.jpg",
= = >   "small": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-250.jpg"
    },
...

这里值得关注的是 "small": "http://coverartarchive.org/release/435fc965-9121-461e-b8da-d9b505c9dc9b/4086974851-250.jpg" 行。
该 URL 是指向 Parachutes 专辑封面的直接链接。

创建和可视化混搭的代码

总体任务是使用 Postman 可视化乐队的所有专辑标题和封面。如何编写代码来实现这一点已在 an answer 中对问题 如何在 Postman 中可视化 API 混搭? 进行了相当详细的描述——因此我将避免在这里进行冗长的讨论,只介绍代码和结果截图:

const lock = setTimeout(() => {}, 43210);
const albumsArray = [];
const urlsArray = [];
const urlOuter = 'https://musicbrainz.org/ws/2/artist/' +
  pm.collectionVariables.get('MBID') + '?fmt=json&inc=url-rels+release-groups';
pm.sendRequest(urlOuter, (_, responseO) => {
  const bandName = responseO.json().name;
  const albums = responseO.json()['release-groups'];
  for (const item of albums) {
    albumsArray.push(item.title);
    urlsArray.push('https://coverartarchive.org/release-group/' + item.id);
  }
  albumsArray.length = urlsArray.length = 15;
  const images = [];
  let countDown = urlsArray.length;
  urlsArray.forEach((url, index) => {
    asynchronousCall(url, imageURL => {
      images[index] = imageURL;
      if (--countDown === 0) { // Callback for ALL starts on next line.
        clearTimeout(lock); // Unlock the timeout.
        const albumTitles = albumsArray.map(value => ({ title: value }));
        const albumImages = images.map(value => ({ image: value }));
        const albumsAndImages = albumTitles.map(
          (item, i) => Object.assign({}, item, albumImages[i]));
        const template = `<table>
          <tr><th>` + bandName + `</th></tr>
          {{#each responseI}}
          <tr><td>{{title}}<br><img src="{{image}}"></td></tr>
          {{/each}}
        </table>`;
        pm.visualizer.set(template, { responseI: albumsAndImages });
      }
    });
  });
  function asynchronousCall (url, callback) {
    pm.sendRequest(url, (_, responseI) => {
      callback(responseI.json().images.find(obj => obj.front === true)
        .thumbnails.small); // Individual callback.
    });
  }
});

结果和文档

https://i.imgur.com/NtwLtvM.png

如何下载和运行 Postman Collection

运行 Postman Collection 应该很简单。
假设您使用的是 the desktop version of Postman,请执行以下操作:

下载 http://henke.atwebpages.com/postman/mbid/MusicBands.pm_coll.json 并将其保存在硬盘上合适的位置。在 Postman 中,Ctrl + O > 上传文件 > MusicBands.pm_coll.json > 导入。您现在应该在 Postman 的收藏中看到 MusicBands。收藏 > MusicBands > DummyRequest > 发送。 8 在 Postman 响应正文中,单击可视化。您现在应该可以滚动 15 个专辑,如上面的屏幕截图所示。

参考

如何从异步调用返回响应?

关于异步调用的一些问答

使用纯 JavaScript 和回调函数

续传风格

XMLHttpRequest:onload 与 onreadystatechange

XMLHttpRequest.responseText

演示 async/await 的示例

拿来

承诺

XMLHttpRequest 标准

获取标准

Web 超文本应用技术工作组 (WHATWG)

指向 ECMA 规范的链接

将值数组转换为对象数组

如何使用 Promise.all 获取 URL 数组?

MusicBrainz API 的文档

封面艺术档案 API 的文档

如何在 Postman 中可视化 API 混搭?

1 原发帖者表示为:它们都返回 undefined
2 如果您认为异步调用令人困惑,请考虑使用查看 some questions and answers about asynchronous calls 看看是否有帮助。
3 XMLHttpRequest 名称与 AJAX 中的 X 一样具有误导性– 现在 Web API 的数据格式普遍是 JSON,而不是 XML。
4 Fetch 返回一个 Promise。我很惊讶地发现 XMLHttpRequestFetch 都不是 ECMAScript 标准的一部分。 JavaScript 可以在这里访问它们的原因是 Web 浏览器提供了它们。 The Fetch Standardthe XMLHttpRequest Standard 均由 2004 年 6 月成立的 the Web Hypertext Application Technology Working Group (WHATWG) 支持。
5 本节借鉴了 How can I fetch an array of URLs with Promise.all? 的很多内容。
6< /sup> 此部分严重依赖 How can I visualize an API mashup in Postman?
7 此 URL 会自动重定向到:https://ia800503.us.archive.org/29/items/mbid-435fc965-9121-461e-b8da-d9b505c9dc9b/index.json
8 如果您收到错误, 运行您的脚本时出现问题,请尝试再次点击发送


P
Peter Mortensen

使用承诺

这个问题的最完美答案是使用 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
});

可是等等...!

使用 Promise 有问题!

为什么要使用我们自己的自定义 Promise?

我使用这个解决方案有一段时间了,直到我发现旧浏览器中存在错误:

未捕获的 ReferenceError:未定义承诺

所以我决定在 JavaScript 编译器下面实现我自己的 ES3 Promise 类,如果它没有定义的话。只需将此代码添加到您的主代码之前,然后安全地使用 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;
    }();
}

我认为您也可以使用回调 :D,但这太不可思议了。
P
Peter Mortensen

当然有很多方法,比如同步请求、promise,但根据我的经验,我认为你应该使用回调方法。 JavaScript 的异步行为是很自然的。

因此,您的代码片段可以重写为有点不同:

function foo() {
    var result;

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

    return result;
}

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

回调或 JavaScript 本质上没有什么异步的。
为什么要保留 var result;return result;?后者仍将总是返回 undefined
P
Peter Mortensen

问题是:

如何从异步调用返回响应?

这可以解释为:

如何使异步代码看起来同步?

解决方案是避免回调,并结合使用 Promises 和 async/await。

我想举一个 Ajax 请求的例子。

(虽然它可以用 JavaScript 编写,但我更喜欢用 Python 编写它,并使用 Transcrypt 将其编译为 JavaScript。它会很清楚。)

让我们首先启用 jQuery 使用,让 $ 作为 S 可用:

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

定义一个返回 Promise 的函数,在本例中为 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")

任何对使用 async / await 感兴趣的人都可能还想阅读 this answer(可能还有我在它下面的评论 :-)。
P
Peter Mortensen

不要向你扔代码,有两个概念是理解 JavaScript 如何处理回调和异步性的关键(这甚至是一个词吗?)

事件循环和并发模型

您需要注意三件事; 队列; the event loop 和堆栈

从广义上讲,事件循环就像项目经理,它不断地监听任何想要运行的函数,并在队列和堆栈之间进行通信。

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

一旦它接收到一条消息来运行某些东西,它就会将它添加到队列中。队列是等待执行的事物的列表(例如您的 AJAX 请求)。想象一下:

使用 foobarFunc 调用 foo.com/api/bar 去执行一个无限循环......等等

当其中一条消息要执行时,它会从队列中弹出消息并创建一个堆栈,堆栈是 JavaScript 执行消息中的指令所需执行的所有内容。所以在我们的例子中,它被告知要调用 foobarFunc

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

所以 foobarFunc 需要执行的任何东西(在我们的例子中是 anotherFunction)都会被压入堆栈。执行,然后忘记 - 事件循环将移动到队列中的下一个事物(或侦听消息)

这里的关键是执行顺序。那是

什么时候运行

当您使用 AJAX 向外部方进行调用或运行任何异步代码(例如 setTimeout)时,JavaScript 依赖于响应才能继续。

最大的问题是它什么时候会得到回应?答案是我们不知道——所以事件循环正在等待该消息说“嘿,快跑吧”。如果 JavaScript 只是同步地等待该消息,您的应用程序将冻结并且它会很糟糕。因此 JavaScript 继续执行队列中的下一项,同时等待消息被添加回队列。

这就是为什么我们使用称为回调的异步功能。 - 一个函数或处理程序,当传递给另一个函数时,将在以后执行。 promise 使用回调(例如传递给 .then() 的函数)作为以更线性的方式推理此异步行为的一种方式。承诺是一种表达“我承诺在某个时候返回一些东西”的方式,而回调是我们处理最终返回的值的方式。 jQuery 使用称为 deffered.done deffered.faildeffered.always (以及其他)的特定回调。你可以看到他们所有here

因此,您需要做的是传递一个函数,该函数承诺在某个时刻使用传递给它的数据执行。

因为回调不会立即执行,而是稍后执行,因此将引用传递给未执行的函数很重要。所以

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

所以大多数时候(但并非总是)你会通过 foo 而不是 foo()

希望这会有些意义。当您遇到类似这样令人困惑的事情时 - 我强烈建议您完整阅读文档以至少了解它。它会让你成为一个更好的开发者。


我正在努力接受“回调有点像承诺”。这就像说“面粉有点像面包”,但事实并非如此。你使用面粉、水和其他配料,混合它们,最终经过一个过程,面包就是结果。
这是真的——我想我试图说一些不太明白我的意思的话。 JS 中的 promise 显然代表了与回调不同的东西,但是在编写任何类型的异步功能时,您将执行回调。 Promise 代表值,但回调是我们需要在将来某个时间点返回时使用该值执行的操作。
如果没有回调来处理解析值