JavaScript Promises 任务调度

在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) { ... }

提供的参数包括:

  • callbacks: 函数列表,每个函数在被调用时返回一个Promise。这些是想要执行的异步任务,它们是有向无环图的节点。
  • 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,并将错误原封不动地传递。

代码使用示例

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

  • 登录用户
  • 获取用户的设置
  • 将用户设置解析为JSON
  • 加载当天的视频(仅限注册用户)
  • 根据用户的设置更改页面背景颜色
  • 如果用户启用了自动播放,则播放视频
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 ]);
沪ICP备2024098111号-1
上海秋旦网络科技中心:上海市奉贤区金大公路8218号1幢 联系电话:17898875485