如何等待一组异步回调函数?

103

我在 JavaScript 中有如下代码:

forloop {
    //async call, returns an array to its callback
}
在所有异步调用完成后,我想计算所有数组中的最小值。如何等待它们全部完成?我的想法是创建一个名为 done 的布尔类型数组,在第 i 个回调函数中将 done[i] 设为 true,然后使用 while(not all are done) {} 来判断是否全部完成。修改:我认为一种可能的但不太好看的解决方案是在每个回调函数中编辑 done 数组,然后从每个回调函数调用一个方法来检查是否所有其他的 done 都已设置,因此完成的最后一个回调函数将调用继续的方法。

1
在异步编程中,您是指等待Ajax请求完成吗? - Peter Aron Zentai
7
请注意,“while(not all are done){ }”不能起作用。当您在忙等待时,您的回调函数都无法运行。 - cHao
是的。我正在等待一个异步调用外部API返回,以便触发回调方法。是的cHao,我意识到了这一点,这就是为什么我在这里寻求帮助的原因:D - codersarepeople
你可以尝试这个:https://github.com/caolan/async 一套非常好用的异步工具函数。 - Paul Greyson
8个回答

203

您的代码并没有十分具体,因此我举一个场景。假设您有10个ajax调用,并且您想要累加这些调用的结果,然后当它们全部完成时执行某些操作。您可以通过在数组中累积数据并跟踪最后一个完成的时间来实现它,如下所示:

手动计数器

var ajaxCallsRemaining = 10;
var returnedData = [];

for (var i = 0; i < 10; i++) {
    doAjax(whatever, function(response) {
        // success handler from the ajax call

        // save response
        returnedData.push(response);

        // see if we're done with the last ajax call
        --ajaxCallsRemaining;
        if (ajaxCallsRemaining <= 0) {
            // all data is here now
            // look through the returnedData and do whatever processing 
            // you want on it right here
        }
    });
}

注意:这里的错误处理很重要(未显示,因为它与您进行ajax调用的方式有关)。您需要考虑如何处理当一个ajax调用永远不会完成的情况,无论是出现错误还是被卡住了很长时间或超时后长时间不响应。


jQuery Promises

补充我在2014年的回答。如今,Promise经常用于解决这类问题,因为jQuery的$.ajax()已经返回了一个Promise,而$.when()会告诉你什么时候一组Promises全部解决,并收集返回结果:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push($.ajax(...));
}
$.when.apply($, promises).then(function() {
    // returned data is in arguments[0][0], arguments[1][0], ... arguments[9][0]
    // you can process it here
}, function() {
    // error occurred
});

ES6标准的Promise

正如kba所述:如果您使用具有内置原生Promise的环境(现代浏览器或node.js或使用babeljs转译或使用promise polyfill),则可以使用ES6规定的Promise。请参见此表格了解浏览器支持情况。除IE外,几乎所有现代浏览器都支持Promise。

如果doAjax()返回一个Promise,则可以这样做:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});
如果你需要将一个非Promise异步操作转换为返回Promise的操作,你可以像这样“promisify”它:
function doAjax(...) {
    return new Promise(function(resolve, reject) {
        someAsyncOperation(..., function(err, result) {
            if (err) return reject(err);
            resolve(result);
        });
    });
}

然后使用上面的模式:

var promises = [];
for (var i = 0; i < 10; i++) {
    promises.push(doAjax(...));
}
Promise.all(promises).then(function() {
    // returned data is in arguments[0], arguments[1], ... arguments[n]
    // you can process it here
}, function(err) {
    // error occurred
});

Bluebird Promises

如果你使用更多功能的库,例如Bluebird promise library,那么它内置了一些额外的函数来使这个过程更加容易:

 var doAjax = Promise.promisify(someAsync);
 var someData = [...]
 Promise.map(someData, doAjax).then(function(results) {
     // all ajax results here
 }, function(err) {
     // some error here
 });

4
@kba - 我不会准确地认为这个答案已经过时,因为所有的技术仍然适用,特别是如果你已经在使用jQuery进行Ajax。但是,我已经以多种方式进行更新,包括原生Promise。 - jfriend00
现在有一个更干净的解决方案,甚至不需要使用jQuery。我正在使用FetchAPI和Promises来完成它。 - philx_x
@philx_x - 你对IE和Safari的支持有什么计划? - jfriend00
@jfriend00 在 Github 上制作了一个 polyfill https://github.com/github/fetch。或者我不确定 Babel 是否已经支持 fetch。https://babeljs.io/ - philx_x
@jfriend00 是的..我的回答可能有点草率,但如果你不是为生产开发而做的话,Promise和fetch()一起使用非常好 :) - philx_x

18
2015年报道:现在大多数最新浏览器(Edge 12、Firefox 40、Chrome 43、Safari 8、Opera 32和Android浏览器4.4.4以及iOS Safari 8.4,但不包括Internet Explorer、Opera Mini和旧版Android)都支持原生Promise
如果我们想执行10个异步操作并在它们全部完成时得到通知,我们可以使用Promise.all原生方法,无需任何外部库。
function asyncAction(i) {
    return new Promise(function(resolve, reject) {
        var result = calculateResult();
        if (result.hasError()) {
            return reject(result.error);
        }
        return resolve(result);
    });
}

var promises = [];
for (var i=0; i < 10; i++) {
    promises.push(asyncAction(i));
}

Promise.all(promises).then(function AcceptHandler(results) {
    handleResults(results),
}, function ErrorHandler(error) {
    handleError(error);
});

2
Promises.all() 应该改为 Promise.all() - jfriend00
1
你的回答还需要提到在哪些浏览器中可以使用Promise.all(),其中不包括当前版本的IE。 - jfriend00

10

您可以使用jQuery的Deferred对象以及when方法。

deferredArray = [];
forloop {
    deferred = new $.Deferred();
    ajaxCall(function() {
      deferred.resolve();
    }
    deferredArray.push(deferred);
}

$.when(deferredArray, function() {
  //this code is called after all the ajax calls are done
});

8
这个问题没有标记为 jQuery,通常意味着提问者不想要一个 jQuery 的答案。 - jfriend00
9
@jfriend00 我不想重复造轮子,因为它已经在jQuery中创建了。 - Paul
4
@Paul,所以与其重新发明轮子,你包含了40kb的垃圾来完成一些简单的事情(延迟) - Raynos
2
但并不是每个人都能或者想要使用jQuery,在SO上的惯例是通过标记问题是否带有jQuery标签来表示。 - jfriend00
@jfriend00,感谢您让我知道这件事。我将来会确保检查标签。 - Paul
4
这个例子中的 $.when 调用是不正确的。要等待一个延迟/承诺数组,您需要使用 $.when.apply($, promises).then(function() { /* do stuff */ })。 - danw

9
您可以这样模拟:
  countDownLatch = {
     count: 0,
     check: function() {
         this.count--;
         if (this.count == 0) this.calculate();
     },
     calculate: function() {...}
  };

然后每个异步调用都会执行以下操作:
countDownLatch.count++;

在每个异步回调方法的结尾添加以下代码:
countDownLatch.check();

换句话说,你模拟了一个倒计时门闩的功能。

在99%的使用情况下,Promise 是最好的选择,但我喜欢这个答案,因为它说明了一种在 Promise polyfill 比使用它的 JS 文件还要大的情况下管理异步代码的方法! - Sukima

6

在我看来,这是最整洁的方式。

Promise.all

FetchAPI

由于某些原因,我在.then函数内使用Array.map不起作用。但您可以使用.forEach和[].concat()或类似的东西。

Promise.all([
  fetch('/user/4'),
  fetch('/user/5'),
  fetch('/user/6'),
  fetch('/user/7'),
  fetch('/user/8')
]).then(responses => {
  return responses.map(response => {response.json()})
}).then((values) => {
  console.log(values);
})

1
我认为应该是 return responses.map(response => { return response.json(); }) 或者 return responses.map(response => response.json()) - user663031

1

使用类似 after 的控制流程库。

after.map(array, function (value, done) {
    // do something async
    setTimeout(function () {
        // do something with the value
        done(null, value * 2)
    }, 10)
}, function (err, mappedArray) {
    // all done, continue here
    console.log(mappedArray)
})

1

我看到有几个回复都用了Promise.all(),但是这个函数会在任何promise生成异常时停止...

2022年最好的解决方案是Promise.allSettled()(文档在这里:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled

一个快速的示例:

const allPromises = [];
for (r in records) {
  const promise = update_async(r);
  allPromises.push(promise);
};
await Promise.allSettled(allPromises);

最后,您将使用allPromises数组获取每个Promise的结果:
  • 当成功时 --> {status: “fulfilled”, value: xxx }
  • 当出现错误时 --> {status: "rejected", reason: Error: an error}

0

在Node.js中,您可以使用async/await来控制异步流程

  • Node.js 7.6支持async/await
  • Node.js v8支持将回调函数转换为Promise的util函数

示例代码:

const foo = async () => {
  try {
    const ids = [100, 101, 102];
    const fetchFromExternalApi = util.promisify(fetchFromExternalApiCallback);
    const promises = ids.map((id) => fetchFromExternalApi(id));
    const dataList = await Promise.resolve(promises); // dataList is an array
    return dataList;
  } catch (err) {
    // error handling
  }
};

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接