跳到主要内容2721. 并行执行异步函数 | 极客日志JavaScriptNode.js大前端算法
2721. 并行执行异步函数
本文介绍了如何在 JavaScript 中不使用内置的 Promise.all() 方法,手动实现并行执行异步函数的功能。核心逻辑是创建一个新 Promise,遍历输入函数数组,同时启动所有异步任务。若所有任务成功,则按顺序返回结果数组;若任一任务失败,则立即拒绝并返回错误原因。文章提供了基于 async/await 和 then/catch 两种语法的实现方案,并分析了时间复杂度 O(N) 和空间复杂度 O(N)。
2721. 并行执行异步函数
一、题目
给定一个异步函数数组 functions,返回一个新的 promise 对象 promise。数组中的每个函数都不接受参数并返回一个 promise。所有的 promise 都应该并行执行。
promise resolve 条件:
- 当所有从
functions 返回的 promise 都成功的并行解析时。promise 的解析值应该是一个按照它们在 functions 中的顺序排列的 promise 的解析值数组。promise 应该在数组中的所有异步函数并行执行完成时解析。
promise reject 条件:
- 当任何从
functions 返回的 promise 被拒绝时。promise 也会被拒绝,并返回第一个拒绝的原因。
请在不使用内置的 Promise.all 函数的情况下解决。
示例 1:
输入:functions = [ () => new Promise(resolve => setTimeout(() => resolve(5), 200)) ]
输出:{"t": 200, "resolved": [5]}
解释:promiseAll(functions).then(console.log);
示例 2:
输入:functions = [ () => new Promise(resolve => setTimeout(() => resolve(1), 200)), () => new Promise((resolve, reject) => ( (), )) ]
输出:{: , : }
解释:由于其中一个 promise 被拒绝,返回的 promise 也在同一时间被拒绝并返回相同的错误。
微信扫一扫,关注极客日志
微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
setTimeout
() =>
reject
"Error"
100
"t"
100
"rejected"
"Error"
输入:functions = [ () => new Promise(resolve => setTimeout(() => resolve(4), 50)), () => new Promise(resolve => setTimeout(() => resolve(10), 150)), () => new Promise(resolve => setTimeout(() => resolve(16), 100)) ]
输出:{"t": 150, "resolved": [4, 10, 16]}
解释:所有的 promise 都成功执行。当最后一个 promise 被解析时,返回的 promise 也被解析了。
- 函数
functions 是一个返回 promise 的函数数组
1 <= functions.length <= 10
二、解决方案
概述
在这个问题中,你需要创建一个名为 promiseAll 的 JavaScript 函数,它模拟 JavaScript 内置的 Promise.all() 方法的行为,但不能使用它。这个函数接受一个包含异步函数的数组作为输入,每个函数返回一个 Promise,然后应该返回一个新的 Promise。
返回的 Promise 仅在输入函数返回的所有 Promise 都成功时才会成功。在这种情况下,Promise 的成功值应该是一个数组,包含所有 Promise 的成功值,顺序与输入数组中相应的函数的顺序相同。然而,如果由输入函数返回的任何 Promise 被拒绝,返回的 Promise 应该立即被拒绝,并携带第一个被拒绝的 Promise 的原因。
有效地解决这个问题需要对 JavaScript 的 Promise 和异步编程有很好的理解。你应该熟悉 Promise 的工作方式,如何创建新的 Promise,以及如何处理 Promise 的解决和拒绝。
在 JavaScript 中使用 Promise
在我们的问题中,我们广泛使用 JavaScript Promise,这是异步编程的基本概念。JavaScript 中的 Promise 表示一个值,它可能不会立即可用,但将来会可用,或者由于错误原因永远不可用。Promise 可以处于三种状态之一:待定(Pending)、已成功(Fulfilled)或已拒绝(Rejected)。
在我们的问题背景下,理解这些状态至关重要。我们正在处理一系列返回 Promise 的函数。我们总是创建一个新的 Promise,这个新 Promise 的状态取决于输入数组中 Promise 的状态。如果输入数组中的所有 Promise 都已成功,那么我们的新 Promise 将使用它们的值解决。如果输入数组中的任何 Promise 被拒绝,我们的新 Promise 将立即被拒绝,并携带第一个被拒绝的 Promise 的原因。
Promise.all()
Promise.all() 是 JavaScript 中的一个内置方法,它接受一个 Promise 可迭代对象,并返回一个新的 Promise。这个新 Promise 仅在可迭代对象中的所有 Promise 都已成功时才会被满足,或者在可迭代对象中的任何 Promise 被拒绝时立即被拒绝。Promise.all() 的 Promise 的值是可迭代对象中已满足的 Promise 的值的数组,按照可迭代对象中 Promise 的顺序排列。
let promise1 = Promise.resolve(3);
let promise2 = 42;
let promise3 = new Promise((resolve, reject) => {
setTimeout(resolve, 100, 'foo');
});
Promise.all([promise1, promise2, promise3]).then((values) => {
console.log(values);
});
正如你所看到的,Promise.all() 在你想要并行运行多个 Promise 并等待它们全部完成时非常有用。它是将 Promise 分组在一起并仅在它们全部准备就绪时处理它们的结果的绝佳方式。
然而,目前的问题要求我们在不使用 Promise.all() 的情况下解决它。这要求我们理解 Promise.all() 的内部工作原理,并通过手动处理 Promise、监视它们的状态以及相应地解决或拒绝最终的 Promise 来模拟它的行为。
还值得一提的是,Promise.all() 存在潜在的问题,需要注意:如果传递给它的 Promise 中的任何一个被拒绝,Promise.all() 将立即以该原因拒绝,丢弃所有其他 Promise,即使它们即将被满足。换句话说,它是一个'全体成功或全体失败'的方法。这实际上是我们的问题要求我们模拟的行为。有关更详细的理解,你可以参考 MDN documentation on Promise.all() 的文档。
JavaScript 中 Promise.all() 的使用场景
运行具有相互依赖的任务
可能存在多个异步任务彼此依赖的情况。在这种情况下,Promise.all() 可能非常有用。你可以同时启动所有任务,然后使用结果数组访问每个任务的结果,以正确的顺序。
let task1 = fetch('/api/task1');
let task2 = fetch('/api/task2');
Promise.all([task1, task2]).then(results => {
let result1 = results[0];
let result2 = results[1];
});
在此示例中,使用 fetch 同时进行两个网络请求。一旦两者都完成,Promise.all() 将解析为一个数组,其中包含两个任务的结果,按照它们添加的顺序排列。这在任务相互依赖但仍然可以并行运行的情况下非常有用。
数据库事务
在数据库操作中,你可能需要执行多个操作,这些操作应该全部成功或全部失败。Promise.all() 允许你将这些操作建模为一个单一的 Promise,该 Promise 在所有操作都成功时或在一个操作失败时立即被拒绝。
let transaction = [UserModel.create({name:'Alice'}), AccountModel.create({userId:'Alice',balance:100})];
Promise.all(transaction).then(()=> console.log('事务成功')).catch(()=> console.log('事务失败'));
在此示例中,我们使用 Promise.all() 执行涉及创建用户和为用户创建帐户的事务。如果其中任何操作失败,Promise.all() 将立即拒绝,允许我们轻松回滚事务。
汇总 API 数据
在实际应用中,你可能需要从多个不同的 API 端点获取数据,然后才能呈现页面或计算一些结果。与等待每个请求完成后才开始下一个请求不同,Promise.all() 允许你同时进行所有请求,然后等待它们全部完成。
let urls = ['https://api.github.com/users/github','https://api.github.com/users/microsoft','https://api.github.com/users/apple'];
Promise.all(urls.map(url=>fetch(url).then(user=> user.json()))).then(users=>{
console.log(users.length);
console.log(users[0]);
});
在此示例中,我们使用 Promise.all() 从多个 GitHub 帐户获取用户数据。这加快了数据获取过程,因为所有请求都同时进行。
方法 1:模拟 Promise.all() 的行为
概述
目标是复制 JavaScript 内置的 Promise.all() 方法的功能。具体来说,我们需要管理一组返回 Promise 的函数,并返回一个 Promise,该 Promise 解析为结果数组,保留原始数组的顺序。我们将自己处理 Promise 的解析,可以使用现代的 async/await 语法或经典的 then/catch 语法。
算法
- 从
promiseAll 函数返回一个新 Promise。
- 如果输入数组为空,立即用一个空数组解析它并返回。
- 初始化一个数组
res 以保存结果,最初填充为 null。
- 初始化一个
resolvedCount 变量,用于跟踪已解析的 Promise 数。
- 迭代 Promise 返回函数的数组。对于每个返回 Promise 的函数:
- 在 async/await 版本中,等待 Promise。在解析时,将结果放入
res 数组中的相应位置并增加 resolvedCount。如果引发错误,立即用错误拒绝 Promise。
- 在 then/catch 版本中,附加一个 then 子句和一个 catch 子句。在解析时,then 子句将结果放入
res 数组中并增加 resolvedCount。catch 子句用错误拒绝 Promise。
如果所有 Promise 都已解析(即 resolvedCount 等于函数数组的长度),则使用 res 数组解析 promiseAll() Promise。
async/await 版本和 then/catch 版本的主要区别在于语法和等待/处理 Promise 的方式,但总体方法保持不变。这两种实现都确保所有 Promise 同时开始(而不是按顺序),并且返回的 Promise 解析为它们的结果数组,保持原始顺序。
实现
实现 1:使用 async/await 语法
var promiseAll = async function(functions) {
return new Promise((resolve, reject) => {
if (functions.length === 0) {
resolve([]);
return;
}
const res = new Array(functions.length).fill(null);
let resolvedCount = 0;
functions.forEach(async (el, idx) => {
try {
const subResult = await el();
res[idx] = subResult;
resolvedCount++;
if (resolvedCount === functions.length) {
resolve(res);
}
} catch (err) {
reject(err);
}
});
});
};
这段代码使用 async/await 语法,它比传统的 Promise 语法更现代,通常更易于阅读。它初始化与输入数组长度相同的空值数组。然后,它使用 forEach 迭代输入数组,运行每个函数,并在解析后将结果数组中相应的空值替换为函数的返回值。如果所有函数都成功解析,则 promiseAll() 返回的 Promise 将与结果数组一起解析。如果任何函数拒绝,promiseAll() 返回的承诺将立即以第一个拒绝的函数提供的原因拒绝。
实现 2:使用 then/catch 语法
var promiseAll = function(functions) {
return new Promise((resolve, reject) => {
if (functions.length === 0) {
resolve([]);
return;
}
const res = new Array(functions.length).fill(null);
let resolvedCount = 0;
functions.forEach((el, idx) => {
el().then((subResult) => {
res[idx] = subResult;
resolvedCount++;
if (resolvedCount === functions.length) {
resolve(res);
}
}).catch((err) => {
reject(err);
});
});
});
};
这段代码与第一个实现非常相似,但使用了传统的 Promise 语法,而不是 async/await。输入数组中的每个函数都会运行,并且会调用 then 方法来处理它们的解析或 catch 方法来处理它们的拒绝。如果所有函数都成功解决,promiseAll() 返回的 Promise 将解析为结果数组。如果任何函数拒绝,promiseAll() 返回的 Promise 将立即拒绝,并携带第一个拒绝的函数提供的原因。
复杂度分析
时间复杂度:O(N),其中 N 是传递给 promiseAll() 的函数数目。这是因为 promiseAll() 本质上是等待所有 N 个 Promise 解析或拒绝,因此时间复杂度与 Promise 数目成正比。请注意,这不包括运行为 Promise 运行的单个函数的时间复杂度,它侧重于 promiseAll() 本身的操作。
空间复杂度:O(N),其中 N 是传递给 promiseAll() 的函数数目。主要用于存储 Promise 结果。与时间复杂度一样,空间复杂度与 Promise 数目成正比。
面试提示:
Promise.all() 是什么,它是如何工作的?Promise.all() 是 JavaScript 中的一个实用函数,它将多个 Promise 聚合成一个单一的 Promise,该 Promise 仅在所有输入 Promise 都已解决时才会解决,或者在输入 Promise 中的任何一个拒绝时立即拒绝。它通常用于需要同时执行多个异步操作,并且进一步的计算取决于这些操作的完成。
- 如果传递给
Promise.all() 的 Promise 中有一个拒绝会发生什么?如果传递给 Promise.all() 的 Promise 中有一个拒绝,Promise.all() 返回的 Promise 将立即被拒绝,并携带第一个拒绝的 Promise 的原因。这种行为有时被称为'快速失败'。
- 如何处理
Promise.all() 中的单个 Promise 拒绝?要处理 Promise.all() 中的单个 Promise 拒绝,你可以捕获单个 Promise 中的错误并将其转换为带有错误值的解决。这样,Promise.all() 将始终解决,而错误处理可以在生成的值数组上执行。但是,从 ECMAScript 2020 开始,更好的选择是使用 Promise.allSettled()。
Promise.all() 和 Promise.allSettled() 之间有什么区别?Promise.allSettled() 方法与 Promise.all() 类似,但有一个关键区别。虽然 Promise.all() 只要其中一个 Promise 拒绝就会拒绝,Promise.allSettled() 在所有 Promise 已解决时或已拒绝时都会解决。Promise.allSettled() 的解析值是一个对象数组,每个对象都描述每个 Promise 的结果。