65.9K
CodeProject 正在变化。 阅读更多。
Home

JavaScript Promise 的灵活调度

starIconstarIconstarIconstarIconemptyStarIcon

4.00/5 (2投票s)

2017年2月27日

LGPL3

5分钟阅读

viewsIcon

6455

downloadIcon

79

将 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.prototype.then() runs many tasks sequentially.

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.all() runs many tasks in parallel.

泛化:任务的有向无环图

我们在本文中编写的函数泛化了 Promise 的链式调用(使用 `Promise.prototype.then()`)和 Promise 的同时执行(使用 `Promise.all()`)。它允许高效地执行一个任务网络,或者如果你愿意,一个任务依赖图。换句话说,我们编写的函数适用于上述情况,也适用于这种情况。

A directed acyclic graph of promises.

函数参数

我们将该函数命名为 `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,并将错误原封不动地传递。

使用代码:一个例子

假设你正在运行一个网站,其中有一个每日视频。当用户访问网站时,需要执行以下操作:

  1. 登录用户
  2. 获取用户设置
  3. 将用户设置解析为 JSON
  4. 加载每日视频(仅限注册用户可用)
  5. 根据用户设置更改页面背景颜色
  6. 如果用户启用了自动播放,则播放视频。

这里的任务及其相互依赖关系得到了说明。

An example network of asynchronous tasks.

以下是如何为这个任务图使用 `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 的有向无环图(在本例中为简单的超时),以及它们的状态。演示代码也包含在本文中。

© . All rights reserved.