【力扣】2721. 并行执行异步函数
文章目录
- [【力扣】2721. 并行执行异步函数](#【力扣】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); // [5]
单个函数在 200 毫秒后以值 5 成功解析。
示例 2:
输入:functions = [
() => new Promise(resolve => setTimeout(() => resolve(1), 200)),
() => new Promise((resolve, reject) => setTimeout(() => reject("Error"), 100))
]
输出:{"t": 100, "rejected": "Error"}
解释:由于其中一个 promise 被拒绝,返回的 promise 也在同一时间被拒绝并返回相同的错误。
示例 3:
输入: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 的原因。
问题描述提供了三个关键示例,以说明预期的功能。在第一个示例中,有一个单一的函数在一定延迟后解决。我们的函数返回的 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 的原因。
为了复习或对于那些对 JavaScript Promise 还不熟悉的人,我们建议查看 30 天 JavaScript 计划中的 Add two promises 编辑。这个教程提供了对 Promise、它们的状态以及它们在 JavaScript 异步编程中的使用的全面解释。
Promise.all()
Promise.all() 是 JavaScript 中的一个内置方法,它接受一个 Promise 可迭代对象,并返回一个新的 Promise。这个新 Promise 仅在可迭代对象中的所有 Promise 都已成功时才会被满足,或者在可迭代对象中的任何 Promise 被拒绝时立即被拒绝。Promise.all() 的 Promise 的值是可迭代对象中已满足的 Promise 的值的数组,按照可迭代对象中 Promise 的顺序排列。
js
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); // [3, 42, "foo"]
});
正如你所看到的,Promise.all() 在你想要并行运行多个 Promise 并等待它们全部完成时非常有用。它是将 Promise 分组在一起并仅在它们全部准备就绪时处理它们的结果的绝佳方式。
然而,目前的问题要求我们在不使用Promise.all()的情况下解决它。这要求我们理解Promise.all()的内部工作原理,并通过手动处理 Promise、监视它们的状态以及相应地解决或拒绝最终的 Promise 来模拟它的行为。
还值得一提的是,Promise.all() 存在潜在的问题,需要注意:如果传递给它的 Promise 中的任何一个被拒绝,Promise.all() 将立即以该原因拒绝,丢弃所有其他 Promise,即使它们即将被满足。换句话说,它是一个"全体成功或全体失败"的方法。这实际上是我们的问题要求我们模拟的行为。有关更详细的理解,你可以参考 MDN documentation on Promise.all() 的的文档。
JavaScript 中Promise.all()的使用场景
-
汇总 API 数据
在实际应用中,你可能需要从多个不同的 API 端点获取数据,然后才能呈现页面或计算一些结果。与等待每个请求完成后才开始下一个请求不同,
Promise.all()允许你同时进行所有请求,然后等待它们全部完成。jslet 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); // 3 console.log(users[0]); // {login: "github", ...} });在此示例中,我们使用
Promise.all()从多个 GitHub 帐户获取用户数据。这加快了数据获取过程,因为所有请求都同时进行。 -
数据库事务
在数据库操作中,你可能需要执行多个操作,这些操作应该全部成功或全部失败。
Promise.all()允许你将这些操作建模为一个单一的 Promise,该 Promise 在所有操作都成功时或在一个操作失败时立即被拒绝。jslet transaction = [ UserModel.create({ name: 'Alice' }), AccountModel.create({ userId: 'Alice', balance: 100 }) ]; Promise.all(transaction) .then(() => console.log('事务成功')) .catch(() => console.log('事务失败'));在此示例中,我们使用
Promise.all()执行涉及创建用户和为用户创建帐户的事务。如果其中任何操作失败,Promise.all() 将立即拒绝,允许我们轻松回滚事务。 -
运行具有相互依赖的任务
可能存在多个异步任务彼此依赖的情况。在这种情况下,
Promise.all()可能非常有用。你可以同时启动所有任务,然后使用结果数组访问每个任务的结果,以正确的顺序。jslet 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()将解析为一个数组,其中包含两个任务的结果,按照它们添加的顺序排列。这在任务相互依赖但仍然可以并行运行的情况下非常有用。
方法 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。
- 在 async/await 版本中,等待 Promise。在解析时,将结果放入
如果所有 Promise 都已解析(即resolvedCount等于函数数组的长度),则使用 res 数组解析promiseAll()Promise。
async/await 版本和 then/catch 版本的主要区别在于语法和等待/处理 Promise 的方式,但总体方法保持不变。这两种实现都确保所有 Promise 同时开始(而不是按顺序),并且返回的 Promise 解析为它们的结果数组,保持原始顺序。
实现
实现 1:使用 async/await 语法
js
var promiseAll = async function(functions) {
return new Promise((resolve,reject) => {
if(functions.length === []) {
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 syntax,它比传统的 Promise 语法更现代,通常更易于阅读。它初始化与输入数组长度相同的空值数组。然后,它使用forEach迭代输入数组,运行每个函数,并在解析后将结果数组中相应的空值替换为函数的返回值。如果所有函数都成功解析,则promiseAll()返回的 Promise 将与结果数组一起解析。如果任何函数拒绝,promiseAll() 返回的承诺将立即以第一个拒绝的函数提供的原因拒绝。
实现 2:使用 then/catch 语法
js
var promiseAll = function(functions) {
return new Promise((resolve,reject) => {
if(functions.length === []) {
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 的结果。