JavaScript Promise 的灵活调度





4.00/5 (2投票s)
将 JavaScript 的 Promise.all() 泛化到有向无环图
引言
JavaScript 中的 Promise 非常适合处理许多异步任务。标准的 `Promise.prototype.then()` 和 `Promise.all()` 函数分别用于调度顺序执行和并行执行的任务。
然而,假设我们需要执行许多异步任务,其中一些任务依赖于其他任务。在这种情况下,`Promise.all()` 由于任务间的依赖关系将无法正常工作,而 `Promise.prototype.then()` 会效率低下,因为我们会不必要地顺序执行任务。
在本文中,我们介绍了一个简单的函数,它可以处理 JavaScript Promise 网络(即异步任务及其依赖关系的 DAG)的最优调度通用情况。
背景:Promise.prototype.then() 和 Promise.all()
JavaScript 语言提供了内置机制,可以将多个异步任务合并到一个 Promise 中。我们将讨论其中两个。
Promise.prototype.then()
也许组合 Promise 最重要的方式是使用 `Promise.prototype.then()` [参考]。它允许将一个 Promise 链接到另一个 Promise,从而使两个异步任务顺序执行。
可以使用 `then()` 来形成更长的顺序执行异步任务链。一个任务的输出将作为下一个任务的输入。这是一个 Promise 链的例子。
// promise that resolves after t milliseconds
// with no value
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.prototype.then()` 的用途。
Promise.all()
组合 Promise 的另一种方法是使用 `Promise.all()` [参考]。它允许从一个 Promise 列表(实际上是可迭代对象)创建一个 Promise,该 Promise:
- 一旦提供的任何 Promise 被拒绝,就立即被拒绝。
- 当所有提供的 Promise 都已解决时,它才被解决。
特别地,所有提供的 Promise 都代表并行运行的任务。以下是 `Promise.all()` 用法的示例。
// promise that resolves after t milliseconds
// with no value
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.all()` 的用途。
泛化:任务的有向无环图
我们在本文中编写的函数泛化了 Promise 的链式调用(使用 `Promise.prototype.then()`)和 Promise 的同时执行(使用 `Promise.all()`)。它允许高效地执行一个任务网络,或者如果你愿意,一个任务依赖图。换句话说,我们编写的函数适用于上述情况,也适用于这种情况。
函数参数
我们将该函数命名为 `promiseDAG()`(DAG 是有向无环图的缩写)。它的样子如下:
function promiseDAG(callbacks, dag) {
...
}
提供的参数是:
- `callbacks`:一个函数列表,每个函数在被调用时都返回一个 Promise。
这些是我们想要执行的异步任务,它们是有向无环图中的节点。 - `dag`:一个列表的列表,指定任务之间的相互依赖关系。
这些指定了有向无环图中的边(并且它们决定了将哪些参数传递给回调函数)。
如果提供了 *n* 个回调函数,则 `dag` 应该是一个包含 *n* 个整数列表的列表。其中 *i* 处的列表应该是整数索引列表,指向 `callbacks`,指定第 *i* 个任务依赖于哪些任务。例如,如果 `dag[i]` 包含整数 *j*,则有向无环图从第 *j* 个任务到第 *i* 个任务有一条边。
上面的网络执行方式如下:
// each of these should return a promise that executes the task
function task0() {
return ...
}
function task1(value0) {
return ...
}
function task2(value0) {
return ...
}
function task3(value1, value2) {
return ...
}
function task4(value2) {
return ...
}
var p = promiseDAG([task0, task1, task2, task3, task4], [[], [0], [0], [1,2], [2]]);
函数行为
当调用 `promiseDAG()` 时,任何没有入边(即不依赖于其他任务)的任务都会启动。每当一个任务完成时,所有先决条件都已完成的任务就会启动。**传递给回调函数的参数正是其先决条件解析出来的值**,顺序与 `dag` 中指定的顺序相同。
**注意**。当 JavaScript 函数的调用参数多于其接受的参数时,多余的参数会被静默忽略。这意味着,如果 taskB 依赖于 taskA,但不需要知道 taskA 解析出来的值,你可以在有向无环图中指示依赖关系,但将 taskB() 编写成一个不带参数的函数。
当所有任务都成功完成后,`promiseDAG()` 返回的 Promise 将得到解决。然后,返回的 Promise 的值是一个与 `callbacks` 长度相同的列表,其中包含所有已解决任务的返回值,按顺序排列。
一旦任何任务失败,`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,并将错误原封不动地传递。
使用代码:一个例子
假设你正在运行一个网站,其中有一个每日视频。当用户访问网站时,需要执行以下操作:
- 登录用户
- 获取用户设置
- 将用户设置解析为 JSON
- 加载每日视频(仅限注册用户可用)
- 根据用户设置更改页面背景颜色
- 如果用户启用了自动播放,则播放视频。
这里的任务及其相互依赖关系得到了说明。
以下是如何为这个任务图使用 `promiseDAG()`:
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
]);
更新与演示
我写这段代码是在学习 JavaScript Promise 的时候。除了可以与本文一起下载外,它还托管在 GitHub 上。
这里有一个使用 `promiseDAG()` 的演示页面。它以图形方式显示了 Promise 的有向无环图(在本例中为简单的超时),以及它们的状态。演示代码也包含在本文中。