你的疑虑很有道理。虽然你提出这个问题已经过去了几年,但我认为值得在现有答案中添加一些内容。
并行运行函数数组,而不等待前一个函数完成。如果任何一个函数将错误传递给其回调…
这句话并不完全正确。实际上,在JavaScript中不可能不等待每个函数完成,因为函数调用和函数返回都是同步和阻塞的。所以当它调用任何函数时,它必须等待函数返回。它无需等待的是传递给该函数的回调的调用。
比喻
不久前,我写了一个简短的故事来演示这个概念:
引用其中的一部分:
“So I said: ‘Wait a minute, you tell me that one cake takes three and a half hours and four cakes take only half an hour more than one? It doesn’t make any sense!’ I though that she must be kidding so I started laughing.”
“But she wasn’t kidding?”
“No, she looked at me and said: ‘It makes perfect sense. This time is mostly waiting. And I can wait for many things at once just fine.’ I stopped laughing and started thinking. It finally started to get to me. Doing four pillows at the same time didn’t buy you any time, maybe it was arguably easier to organize but then again, maybe not. But this time it was something different. But I didn’t really know how to use that knowledge yet.”
Theory
我认为强调单线程事件循环中你永远不能同时做多于一件事情是很重要的。但是,你可以很好地等待许多事情。这就是这里发生的事情。
Async模块中的并行函数按顺序调用每个函数,但每个函数必须在下一个函数被调用之前返回,这是无法避免的。这里的魔法在于函数在返回之前并没有真正执行其任务 - 它只是安排了一些任务、注册了事件监听器、将某些回调传递到其他地方、向某些承诺添加了解决处理程序等。
然后,当预定的任务完成时,之前由该函数注册的某个处理程序被执行,这反过来又执行了最初由Async模块传递的回调函数,并且Async模块知道这个函数已经完成了 - 这次不仅仅是指它已经返回,而且还意味着传递给它的回调函数终于被调用了。
例如,假设您有3个函数分别下载3个不同的URL:getA()、getB()和getC()。
我们将编写
Request模块的模拟代码来模拟请求和一些延迟。
function mockRequest(url, cb) {
const delays = { A: 4000, B: 2000, C: 1000 };
setTimeout(() => {
cb(null, {}, 'Response ' + url);
}, delays[url]);
};
现在有三个功能基本相同,采用详细记录方式:
function getA(cb) {
console.log('getA called');
const url = 'A';
console.log('getA runs request');
mockRequest(url, (err, res, body) => {
console.log('getA calling callback');
cb(err, body);
});
console.log('getA request returned');
console.log('getA returns');
}
function getB(cb) {
console.log('getB called');
const url = 'B';
console.log('getB runs request');
mockRequest(url, (err, res, body) => {
console.log('getB calling callback');
cb(err, body);
});
console.log('getB request returned');
console.log('getB returns');
}
function getC(cb) {
console.log('getC called');
const url = 'C';
console.log('getC runs request');
mockRequest(url, (err, res, body) => {
console.log('getC calling callback');
cb(err, body);
});
console.log('getC request returned');
console.log('getC returns');
}
最后,我们使用 async.parallel
函数来调用它们:
async.parallel([getA, getB, getC], (err, results) => {
console.log('async.parallel callback called');
if (err) {
console.log('async.parallel error:', err);
} else {
console.log('async.parallel results:', JSON.stringify(results));
}
});
立即显示的内容是这样的:
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getC called
getC runs request
getC request returned
getC returns
正如您所看到的,这是一个连续的过程 - 函数被依次调用,前一个函数返回后才会调用下一个函数。然后我们加入一些延迟来验证:
getC calling callback
getB calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]
所以,
getC
先完成,然后是
getB
和
getC
- 最后一个完成后,
async.parallel
就会调用我们的回调函数,并将所有响应组合成正确的顺序 - 按照我们指定的函数顺序,而不是请求完成的顺序。
此外,我们可以看到程序在大约最长请求所花费的时间 4.071 秒后结束,因此我们可以看到这些请求都是同时进行的。
现在,让我们使用
async.parallelLimit
在最多 2 个并行任务的限制下运行它:
async.parallelLimit([getA, getB, getC], 2, (err, results) => {
console.log('async.parallel callback called');
if (err) {
console.log('async.parallel error:', err);
} else {
console.log('async.parallel results:', JSON.stringify(results));
}
});
现在有一点不同。我们立即看到的是:
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
所以getA
和getB
被调用并返回了,但getC
还没有被调用。然后在一段时间后我们看到:
getB calling callback
getC called
getC runs request
getC request returned
getC returns
这表明一旦
getB
调用回调函数,Async模块就不再有2个正在进行的任务,而只剩下1个,可以开始另一个任务
getC
,并立即执行它。然后,再加上其他延迟,我们会看到:
getC calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]
这个过程和async.parallel
例子中的一样,完成了整个过程。这次整个过程也大约用了4秒钟,因为延迟调用getC
并没有带来任何影响——它仍然在第一个调用getA
结束之前完成了。
但是如果我们把延迟时间改成这些:
const delays = { A: 4000, B: 2000, C: 3000 };
那么情况就不同了。现在使用 async.parallel
需要 4 秒,但是带有限制的 async.parallelLimit
(限制为 2)需要 5 秒,并且顺序略有不同。
无限制:
$ time node example.js
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getC called
getC runs request
getC request returned
getC returns
getB calling callback
getC calling callback
getA calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]
real 0m4.075s
user 0m0.070s
sys 0m0.009s
限制为2:
$ time node example.js
getA called
getA runs request
getA request returned
getA returns
getB called
getB runs request
getB request returned
getB returns
getB calling callback
getC called
getC runs request
getC request returned
getC returns
getA calling callback
getC calling callback
async.parallel callback called
async.parallel results: ["Response A","Response B","Response C"]
real 0m5.075s
user 0m0.057s
sys 0m0.018s
概述
我认为无论你是使用类似于回调函数的方式,还是使用 promises 或 async/await,最重要的一点是,在单线程事件循环中你只能一次做一件事情,但是你可以同时等待许多事情。