在JavaScript编程中,异步任务的处理是常见的需求。通常,使用Promise.prototype.then()
来顺序执行任务,使用Promise.all()
来并行执行任务。然而,当面对许多异步任务,且这些任务之间存在依赖关系时,Promise.all()
由于任务之间的依赖性而无法使用,而Promise.prototype.then()
则会因为不必要的顺序执行而效率低下。本文将介绍一种简单的函数,用于最优地调度一个网络中的相互依赖的JavaScriptPromises(即异步任务和它们之间的依赖关系构成的有向无环图)。
JavaScript提供了内置机制来将多个异步任务合并成一个Promise。讨论其中的两种方式:
Promise.prototype.then()
使用Promise.prototype.then()
是合并Promise的一种重要方式。它允许一个Promise链接到另一个Promise,使得两个异步任务顺序执行。可以利用then()
形成更长的顺序执行异步任务链。一个任务的输出将作为下一个任务的输入。以下是一个Promise链式调用的例子:
function sleep(t) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("resolved", t);
resolve();
}, t);
});
}
sleep(3000)
.then((value) => {
return sleep(2000);
})
.then((value) => {
return sleep(1000);
})
.then((value) => {
console.log("done!");
});
Promise.all()
另一种合并Promise的方式是使用Promise.all()
。它允许从一组Promise中创建一个Promise,该Promise会在所有提供的Promise都解决时解决,或者在任何一个提供的Promise被拒绝时立即被拒绝。所有提供的Promise代表的任务都是并行运行的。以下是一个使用Promise.all()
的例子:
function sleep(t) {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log("resolved", t);
resolve();
}, t);
});
}
Promise.all([sleep(3000), sleep(2000), sleep(1000)])
.then((value) => {
console.log("done!");
});
本文介绍的函数泛化了使用Promise.prototype.then()
进行Promise链式调用和使用Promise.all()
同时执行Promise。它允许高效地执行任务网络,或者换句话说,任务的依赖图。也就是说,正在编写的函数适用于上述情况,但也适用于这种情况:
将这个函数称为promiseDAG()
(DAG代表有向无环图)。它看起来像这样:
function promiseDAG(callbacks, dag) {
...
}
提供的参数包括:
如果提供了n个回调函数,那么dag应该是n个列表的列表。第i个列表应该是一个整数列表,这些整数是callbacks的索引,指定第i个任务依赖于哪些任务。例如,如果dag[i]包含整数j,那么有向无环图有一个从第j个任务到第i个任务的边。
当调用promiseDAG()
时,任何没有入边的任务(即不依赖于其他任务的任务)都会开始执行。每当一个任务完成时,任何所有先决条件都已完成的任务现在都会开始执行。
提供给回调函数的参数正是其先决条件解决的值,按照在dag中指定的顺序。
注意:当JavaScript函数被调用时,如果传入的参数比它接受的多,多余的参数会被静默忽略。这意味着如果任务B依赖于任务A,但不需要知道任务A解决的值,可以在有向无环图中指示依赖关系,但将taskB()编写为一个不带参数的函数。
当所有任务都成功完成时,由promiseDAG()
返回的Promise会被解决。返回的Promise的值是一个与callbacks长度相同的列表,包含所有已解决Promise的任务的返回值,按顺序排列。
一旦任何任务失败,由promiseDAG()
返回的Promise会被拒绝。错误将与失败任务被拒绝的错误相同。不会再启动新任务(尽管当前正在运行的任务将继续运行,因为没有办法取消一个待处理的Promise)。
让看看promiseDAG()
的内部工作原理。该函数的结构如下:
function promiseDAG(callbacks, dag) {
return new Promise((resolve, reject) => {
var N = callbacks.length;
var counts = dag.map((x) => x.length);
// extra variables here
function handleResolution(promise, i, value) {
...
}
function handleRejection(promise, i, error) {
...
}
// start all tasks that have no incoming arrows
for (let i = 0; i < N; ++i) {
if (counts[i] > 0) {
continue;
}
var promise = callbacks[i]();
promise.then((value) => { handleResolution(promise, i, value); }, (error) => { handleRejection(promise, i, error); });
}
});
}
函数handleResolution()
将注册一个Promise解决的值,并启动任何现在其先决条件已满足的Promise(除非一个Promise已经被拒绝,这种情况下不会启动新任务)。
函数handleRejection()
将简单地拒绝由promiseDAG()
构造的Promise,并将错误原封不动地传递。
假设正在运行一个网站,每天有一个视频。当用户访问该网站时,需要执行以下操作:
function login() {
return ...
// a promise that resolves to the username on successful login
}
function fetchSettings(username) {
return fetch('./settings/' + username, {method: 'get'});
}
// the argument received here is a Response from fetch
function parseSettings(settings) {
return settings.json();
}
// ignore the username argument, since we don't need it
function loadVideo() {
return new Promise((resolve, reject) => {
var video = document.createElement("video");
video.addEventListener("canplay", resolve(video));
// resolve when ready to play
video.src = "video.mp4";
});
}
// the argument received here is the settings as JSON
async function setBackground(settings) {
document.body.style.background = settings.favoritecolor;
}
async function play(video, settings) {
if (settings.autoplay) {
video.play();
}
}
promiseDAG([login,
// 0
fetchSettings,
// 1
parseSettings,
// 2
loadVideo,
// 3
setBackground,
// 4
play,
// 5
],
[[], [
0
], [
1
], [
0
], [
2
], [
3,
2
],
// match order of arguments
]);