【力扣】2721. 并行执行异步函数

【力扣】2721. 并行执行异步函数

文章目录

  • [【力扣】2721. 并行执行异步函数](#【力扣】2721. 并行执行异步函数)
    • 一、题目
    • 二、解决方案
      • 概述
      • [在 JavaScript 中使用 Promise](#在 JavaScript 中使用 Promise)
      • [JavaScript 中` Promise.all() `的使用场景](#JavaScript 中Promise.all()的使用场景)
      • [方法 1:模拟 `Promise.all() `的行为](#方法 1:模拟 Promise.all() 的行为)

一、题目

给定一个异步函数数组 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()的使用场景

  1. 汇总 API 数据

    在实际应用中,你可能需要从多个不同的 API 端点获取数据,然后才能呈现页面或计算一些结果。与等待每个请求完成后才开始下一个请求不同,Promise.all() 允许你同时进行所有请求,然后等待它们全部完成。

    js 复制代码
    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); // 3
      console.log(users[0]); // {login: "github", ...}
    });

    在此示例中,我们使用 Promise.all() 从多个 GitHub 帐户获取用户数据。这加快了数据获取过程,因为所有请求都同时进行。

  2. 数据库事务

    在数据库操作中,你可能需要执行多个操作,这些操作应该全部成功或全部失败。Promise.all() 允许你将这些操作建模为一个单一的 Promise,该 Promise 在所有操作都成功时或在一个操作失败时立即被拒绝。

    js 复制代码
    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() 将立即拒绝,允许我们轻松回滚事务。

  3. 运行具有相互依赖的任务

    可能存在多个异步任务彼此依赖的情况。在这种情况下,Promise.all() 可能非常有用。你可以同时启动所有任务,然后使用结果数组访问每个任务的结果,以正确的顺序。

    js 复制代码
    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() 将解析为一个数组,其中包含两个任务的结果,按照它们添加的顺序排列。这在任务相互依赖但仍然可以并行运行的情况下非常有用。

方法 1:模拟 Promise.all() 的行为

概述

目标是复制 JavaScript 内置的 Promise.all() 方法的功能。具体来说,我们需要管理一组返回 Promise 的函数,并返回一个 Promise,该 Promise 解析为结果数组,保留原始数组的顺序。我们将自己处理 Promise 的解析,可以使用现代的 async/await 语法或经典的 then/catch 语法。

算法
  1. promiseAll函数返回一个新 Promise。
  2. 如果输入数组为空,立即用一个空数组解析它并返回。
  3. 初始化一个数组 res 以保存结果,最初填充为 null。
  4. 初始化一个 resolvedCount 变量,用于跟踪已解析的 Promise 数。
  5. 迭代 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 语法
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 数目成正比。

面试提示:
  1. Promise.all() 是什么,它是如何工作的?

    Promise.all() 是 JavaScript 中的一个实用函数,它将多个 Promise 聚合成一个单一的 Promise,该 Promise 仅在所有输入 Promise 都已解决时才会解决,或者在输入 Promise 中的任何一个拒绝时立即拒绝。它通常用于需要同时执行多个异步操作,并且进一步的计算取决于这些操作的完成。

  2. 如果传递给Promise.all()的 Promise 中有一个拒绝会发生什么?

    如果传递给 Promise.all() 的 Promise 中有一个拒绝,Promise.all() 返回的 Promise 将立即被拒绝,并携带第一个拒绝的 Promise 的原因。这种行为有时被称为"快速失败"。

  3. 如何处理Promise.all()中的单个 Promise 拒绝?

    要处理Promise.all()中的单个 Promise 拒绝,你可以捕获单个 Promise 中的错误并将其转换为带有错误值的解决。这样,Promise.all() 将始终解决,而错误处理可以在生成的值数组上执行。但是,从 ECMAScript 2020 开始,更好的选择是使用 Promise.allSettled()

  4. Promise.all() Promise.allSettled()之间有什么区别?

    Promise.allSettled() 方法与Promise.all()类似,但有一个关键区别。虽然 Promise.all() 只要其中一个 Promise 拒绝就会拒绝,Promise.allSettled() 在所有 Promise 已解决时或已拒绝时都会解决。Promise.allSettled() 的解析值是一个对象数组,每个对象都描述每个 Promise 的结果。

相关推荐
进击的小头2 小时前
实战案例:51单片机低功耗场景下的简易滤波实现
c语言·单片机·算法·51单片机
有位神秘人2 小时前
Android中Notification的使用详解
android·java·javascript
phltxy3 小时前
Vue 核心特性实战指南:指令、样式绑定、计算属性与侦听器
前端·javascript·vue.js
咖丨喱3 小时前
IP校验和算法解析与实现
网络·tcp/ip·算法
Byron07074 小时前
Vue 中使用 Tiptap 富文本编辑器的完整指南
前端·javascript·vue.js
罗湖老棍子4 小时前
括号配对(信息学奥赛一本通- P1572)
算法·动态规划·区间dp·字符串匹配·区间动态规划
css趣多多4 小时前
地图快速上手
前端
zhengfei6114 小时前
面向攻击性安全专业人员的一体化浏览器扩展程序[特殊字符]
前端·chrome·safari
fengfuyao9854 小时前
基于MATLAB的表面织构油润滑轴承故障频率提取(改进VMD算法)
人工智能·算法·matlab