JavaScript/TypeScript异步任务并发实用指南

相互独立的任务非常适合并发执行。尽管 JS/TS 是"单线程"的,但在 事件循环(Event Loop)机制 的加持下,异步任务的并发处理能力依然不容小觑。借此机会,我们来深入探讨 JS/TS 中常用的异步任务并发核心的或者重要的知识点。

1 异步核心概念:Promise、async/await及其关系

Promise 是一个表示异步操作结果的对象,它有三种状态:等待(Pending)、完成(Fulfilled)、失败(Rejected) 。通过 .then 处理成功,.catch 处理失败,Promise 让异步代码更清晰。你可以把它想象成一个"承诺",表示某个操作将来会完成(成功或失败),并返回结果。

既然 Promise 是异步编程的核心,那么 async/await 就是它的最佳搭档。async/await 是 ES2017 引入的语法糖,用于简化 Promise 的使用。其核心思想是:用 async 标记的函数会自动返回一个 Promise;用 await 关键字可以暂停函数的执行,直到 Promise 完成(成功或失败)。

async 函数总是返回一个 Promise。即使你直接返回一个非 Promise 的值,它也会被隐式地包装成一个 Promiseawait 关键字后面必须是一个 Promise。它会暂停函数的执行,直到 Promise 完成,然后返回 Promise 的结果。

说得更直白点,async/await 是一种包装好的 Promise 的语法糖。它让异步代码写起来更像同步代码,同时保留了 Promise 的强大功能。

2 异步任务并发常用方法

在 JS/TS 中,处理并发任务时,Promise 提供了多个实用的方法,包括 Promise.allPromise.allSettledPromise.racePromise.any。它们各自有不同的适用场景和行为特点。

2.2 Promise.all

2.2.1 特性

  • 适用于 多个异步任务并行执行,所有任务完成后返回所有成功的结果。

  • 如果 任意一个 Promise 失败 ,则整个 Promise.all 立即失败(短路机制)。

  • 返回值是 一个数组 ,包含所有 Promisevalue,但如果有一个 Promise 失败,则直接抛出该错误。

  • 不会阻止已开始执行的 Promise,即使某个任务已失败,其他任务仍会继续执行。

  • 只要有一个 Promise 失败,整个 Promise.all 立即 reject,但未完成的任务不会被中断,需手动清理可能存在的副作用(如定时器、网络请求等)。

js 复制代码
"use strict";
const p1 = new Promise((resolve) => setTimeout(() => resolve("P1任务延迟3秒后执行成功"), 3000));
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2任务延迟5秒后执行成功"), 5000));
const p3 = new Promise((_, reject) => setTimeout(() => reject("P3任务延迟4秒后执行失败"), 4000));

Promise.all([p1,p2,p3])
    .then((res) => console.log("任务成功:",res))
    .catch((err)=>console.log("任务失败:",err)) // 失败

2.2.2 适用场景/最佳实践

  • 所有任务必须成功的场景

    • 适用于需要 所有异步任务都成功 才能继续下一步的业务逻辑。例如,批量数据请求、多个依赖任务的并行执行。
    • 示例:下载多个资源文件,任何一个文件下载失败都应视为整体失败。
  • 提升性能,避免等待顺序执行

    • 适用于 多个独立、无依赖 的异步操作,使用 Promise.all 可以 并行执行,提升执行效率。
    • 示例:前端页面初始化时并行请求用户信息、配置数据、权限数据,减少整体等待时间。
  • 批量处理、批量校验

    • 适用于需要同时执行 多个异步验证,并在全部验证通过后统一处理结果。
    • 示例:用户注册时并行验证邮箱、手机号、用户名是否可用,只有全部验证成功才继续。
  • 多接口数据聚合

    • 适用于从 多个数据源 获取数据并在返回后进行统一处理。
    • 示例:电商平台同时获取商品详情、用户评价、库存信息,统一展示给用户。
  • 执行时间监控

    • 可与 Promise.race 结合使用,实现超时控制,确保并行任务在规定时间内完成,否则抛出错误。
    • 示例:并行抓取多服务器数据,若未在规定时间内完成,终止操作并返回超时提示。

2.2.3 优化点

不太优雅的牵一发而动全身

牵一发而动全身,任意一个任务失败会影响返回结果甚至是后续的任务执行

可以尝试的更优雅的方式
将Promise.all替换为Promise.allSettled
  • 原因如下文介绍Promise.allSettled时所述,此处不赘述
对子Promise进行catch包装
  • 示例代码如下
js 复制代码
"use strict";
const p1 = new Promise((resolve) => setTimeout(() => resolve("P1任务延迟3秒后执行成功"), 3000));
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2任务延迟5秒后执行成功"), 5000));
const p3 = new Promise((_, reject) => setTimeout(() => reject("P3任务延迟4秒后执行失败"), 4000));

// 对异步任务进行包装,即使报错,也不至于让整个任务失败
const safePromise = (promise) => {
    return promise.then((res) => ({ status: 'success', data: res })).catch((err) => ({ status: 'failed', data: err }));
};

Promise.all([safePromise(p1), safePromise(p2), safePromise(p3)])
    .then((res) => console.log("任务成功:", res))
    .catch((err) => console.log("任务失败:", err));
给Promise失败提供默认值
  • 示例代码如下
js 复制代码
"use strict";
const p1 = new Promise((resolve) => setTimeout(() => resolve("P1任务延迟3秒后执行成功"), 3000));
const p2 = new Promise((resolve) => setTimeout(() => resolve("P2任务延迟5秒后执行成功"), 5000));
const p3 = new Promise((_, reject) => setTimeout(() => reject("P3任务延迟4秒后执行失败"), 4000));

// 给Promise失败提供默认值
const promiseWithDefault = (promise, defaultValue) => {
    return promise.catch(() => defaultValue);
};
Promise.all([promiseWithDefault(p1, "p1"), promiseWithDefault(p2, 'p2'), promiseWithDefault(p3, 'p3')])
    .then((res) => console.log("任务成功:", res))
    .catch((err) => console.log("任务失败:", err));
使用async/await手动处理异常
  • 示例代码如下:
js 复制代码
async function runTasks() {
    try {
        const results = await Promise.all([p1, p2, p3]);
        console.log("所有任务成功", results);
    }
    catch (error) {
        console.log("某个任务失败", error);
    }
}

await runTasks()

2.3 Promise.allSettled

2.3.1 特性

  • 不会因某个任务失败而中断 ,会等待所有 Promise 结束。

  • 每个任务的结果都会返回,包括 fulfilledrejected 状态。

  • 返回值是 一个数组 ,每个元素是包含 statusvalue(成功)或 reason(失败)的对象。

  • 所有任务都会执行,无论成功与否,适合需要全面了解每个任务执行结果的场景。

2.3.2 适用场景/最佳实践

  • 监控多个异步任务的执行情况,不关心个别任务是否失败,所有任务的执行结果都需要收集并处理的场景。

  • 资源清理:无论任务成功或失败,都需要执行清理操作。

  • 日志记录:适用于需要记录所有异步任务执行情况的需求。

2.3.3 代码示例

js 复制代码
const tasks = [
  Promise.resolve('任务1 成功'),
  Promise.reject('任务2 失败'),
  Promise.resolve('任务3 成功'),
];

Promise.allSettled(tasks).then(results => {
  results.forEach((result, index) => {
    if (result.status === 'fulfilled') {
      console.log(`任务 ${index + 1} 成功:`, result.value);
    } else {
      console.log(`任务 ${index + 1} 失败:`, result.reason);
    }
  });
});

2.4 Promise.race

2.4.1 特性

  • 谁先完成(成功或失败) ,就以该 Promise 的结果为准,其他未完成的任务会被忽略。

  • 返回值是第一个完成的 Promise 结果 (不论 fulfilled 还是 rejected)。

  • 适用于竞态(Race Condition)场景,只需要第一个完成的结果,忽略其他任务。

  • 不会取消未完成的 Promise,如果有副作用,需手动清理(如定时器、网络请求等)。

2.4.2 适用场景/最佳实践

  • 超时控制 :与 Promise 结合,设置超时机制,若某任务未及时完成,则用超时结果替代。

  • 快速响应:多个数据源竞争,谁先返回就采用谁的结果(如多CDN)。

  • 资源竞争:适用于同时尝试多个资源,获取最快的可用结果。

2.4.3 代码示例

js 复制代码
const task1 = new Promise((resolve) => {
  setTimeout(() => resolve('任务1 完成'), 3000); // 3秒后完成
});

const task2 = new Promise((resolve) => {
  setTimeout(() => resolve('任务2 完成'), 1000); // 1秒后完成
});

const task3 = new Promise((reject) => {
  setTimeout(() => reject('任务3 失败'), 2000); // 2秒后失败
});

Promise.race([task1, task2, task3])
  .then((result) => {
    console.log('第一个完成的任务结果:', result);
  })
  .catch((error) => {
    console.error('第一个失败的任务原因:', error);
  }); // 第一个完成的任务结果: 任务2 完成

2.5 Promise.any

2.5.1 特性

  • 第一个成功的 Promise 就会返回其结果,忽略失败的任务。

  • 如果所有 Promise 都失败 ,返回一个 AggregateError,包含所有错误信息。

  • 返回值是第一个 fulfilled 的结果 ,与 Promise.race 不同,Promise.any 忽略 rejected

  • 不会取消未完成的 Promise,如果有副作用,需手动清理。

2.5.2 适用场景/最佳实践

  • 容错处理 :在多个异步任务中,只要有一个成功,就能继续执行后续逻辑。

  • 备选方案 :尝试多个操作,只要有一个成功,就采用其结果(如多个 API 请求)。

  • 主备切换:主任务失败时,使用备选任务的结果作为兜底方案。

2.5.3 代码示例

js 复制代码
const p1 = new Promise((resolve)=>setTimeout(()=>resolve("P1任务延迟3秒后执行成功"),3000))
const p2 = new Promise((resolve)=>setTimeout(()=>resolve("P2任务延迟5秒后执行成功"),5000))
const p3 = new Promise((resolve,reject)=>setTimeout(()=>reject("P3任务延迟4秒后执行失败"),4000))


Promise.all([p1,p2,p3])
    .then((res) => console.log("任务成功:",res))
    .catch((err)=>console.log("任务失败:",err))  // 任务失败: P3任务延迟4秒后执行失败

3 异步并发任务规避踩坑

在进行并发任务时,常常会涉及到定时器网络请求文件句柄 等资源。如果没有在任务完成或中断时及时清理,可能会导致内存泄漏任务重复执行数据不一致等问题。

3.1 资源释放

使用 AbortController 清理定时器

  1. 任务竞争Promise.race() 会返回第一个完成的 Promise,无论其是 fulfilled 还是 rejected
  2. 及时中止 :使用 AbortController 监听任务状态,及时清理多余的异步任务,防止资源泄漏。
  3. 避免多次触发:确保在任务完成或失败后,其他未完成的任务被中止。
js 复制代码
const controller = new AbortController();
const { signal } = controller;
let myInterval1;
const p7 = setInterval(() => {
    if (signal.aborted) {
        clearInterval(p7);
        return;
    }
    console.log('正在检查心跳');
}, 1500);
signal.addEventListener("abort", () => {
    clearInterval(p7);
});
const p8 = new Promise((resolve) => setTimeout(() => resolve("复杂计算任务返回结果:Hello world"), 5000));
const p9 = new Promise((_, reject) => setTimeout(() => reject("failed"), 7000));
Promise.race([p8, p9])
    .then(res => {
    console.log('有任务执行成功', res);
    controller.abort();
})
    .catch(err => {
    console.log("有任务执行失败");
    controller.abort();
});

3.2 副作用消除

异步任务如果不加管理,容易引入副作用。例如,未及时取消的网络请求可能导致数据污染资源浪费

并发请求时清理未完成任务

  1. 防止数据竞争:当一个任务成功时,其他未完成的任务会被中止,避免多个请求竞争修改数据。
  2. 任务粒度控制:及时终止不再需要的异步操作,降低系统负担。
  3. 兼顾成功与失败:不论任务成功还是失败,均确保释放资源,防止资源浪费。
js 复制代码
const controller = new AbortController();
const { signal } = controller;

// 模拟异步任务 P1 (远程请求)
const P1 = fetch('https://jsonplaceholder.typicode.com/todos/1', { signal })
  .then(response => response.json())
  .then(data => {
    console.log("P1: 请求远程资源完成", data);
    return "P1 远程资源";
  })
  .catch((err) => {
    console.error("P1 请求失败", err);
    throw err;
  });

// 模拟异步任务 P2 (远程请求)
const P2 = fetch('https://jsonplaceholder.typicode.com/todos/2', { signal })
  .then(response => response.json())
  .then(data => {
    console.log("P2: 请求远程资源完成", data);
    return "P2 远程资源";
  })
  .catch((err) => {
    console.error("P2 请求失败", err);
    throw err;
  });

// 模拟异步任务 P3 (远程请求)
const P3 = fetch('https://jsonplaceholder.typicode.com/todos/3', { signal })
  .then(response => response.json())
  .then(data => {
    console.log("P3: 请求远程资源完成", data);
    return "P3 远程资源";
  })
  .catch((err) => {
    console.error("P3 请求失败", err);
    throw err;
  });

// 模拟本地加载任务 P4
const P4 = new Promise((resolve) => {
  setTimeout(() => {
    console.log("P4: 本地资源加载完成");
    resolve("P4 本地资源");
  }, 6000); // 模拟6秒的本地加载
});

// 任务的竞争,使用 Promise.race
Promise.race([P1, P2, P3, P4])
  .then((result) => {
    console.log("获胜任务:", result);
    // 清理和释放资源:成功的任务资源释放
    controller.abort();  // 取消其他任务
    console.log("资源已清理!");
  })
  .catch((err) => {
    console.log("有任务失败", err);
    controller.abort();  // 取消其他任务
    console.log("资源已清理!");
  });

3.3 其他常见坑及规避策略

1️⃣ 未捕获的异常

如果异步任务执行中发生未捕获的异常,可能会导致程序崩溃。请确保对每个 Promise 链添加 .catch(),并使用 try...catch 捕获 await 中的错误。

解决方案

javascript 复制代码
const safeAsync = async (promise) => {
  try {
    return await promise;
  } catch (err) {
    console.error("异步任务出错:", err);
    return null; // 或自定义错误处理
  }
};

2️⃣ 竞态条件(Race Condition)

多个异步任务可能同时修改共享资源,导致数据不一致。例如,表单重复提交、异步更新状态等问题。

解决方案

  • 使用锁机制确保同一时间只有一个任务执行。
  • 通过AbortController 取消不必要的任务。

3️⃣ 内存泄漏

长时间未释放的异步任务、未清理的事件监听器、引用未解除等,可能导致内存泄漏。

解决方案

  • 使用 AbortController 取消任务。
  • 对 DOM 事件进行解绑,如 element.removeEventListener()
  • 使用 WeakMap 处理弱引用,避免对象被意外保留。

4️⃣ 异步任务顺序依赖

当异步任务具有强依赖关系时,若没有按顺序执行,可能引起逻辑错误。

解决方案

  • 使用 async/await 强制任务按序执行。
  • 使用 Promise.all() 并行执行但依赖彼此结果的任务。

在现代 JS/TS 开发中,异步任务无处不在,尤其在处理 I/O 操作(如网络请求、文件读取、数据库操作等)时,合理管理和释放资源至关重要。系统梳理核心知识点,能帮助我们更深入理解和高效使用异步并发操作,从而编写出更加健壮、可维护的代码。希望这些内容对你有所启发,欢迎交流与探讨!

相关推荐
编程猪猪侠17 分钟前
Tailwind CSS 自定义工具类与主题配置指南
前端·css
qhd吴飞21 分钟前
mybatis 差异更新法
java·前端·mybatis
YGY Webgis糕手之路42 分钟前
OpenLayers 快速入门(九)Extent 介绍
前端·经验分享·笔记·vue·web
患得患失9491 小时前
【前端】【vueDevTools】使用 vueDevTools 插件并修改默认打开编辑器
前端·编辑器
ReturnTrue8681 小时前
Vue路由状态持久化方案,优雅实现记住表单历史搜索记录!
前端·vue.js
UncleKyrie1 小时前
一个浏览器插件帮你查看Figma设计稿代码图片和转码
前端
遂心_1 小时前
深入解析前后端分离中的 /api 设计:从路由到代理的完整指南
前端·javascript·api
你听得到111 小时前
Flutter - 手搓一个日历组件,集成单日选择、日期范围选择、国际化、农历和节气显示
前端·flutter·架构
风清云淡_A1 小时前
【REACT18.x】CRA+TS+ANTD5.X封装自定义的hooks复用业务功能
前端·react.js
@大迁世界1 小时前
第7章 React性能优化核心
前端·javascript·react.js·性能优化·前端框架