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 操作(如网络请求、文件读取、数据库操作等)时,合理管理和释放资源至关重要。系统梳理核心知识点,能帮助我们更深入理解和高效使用异步并发操作,从而编写出更加健壮、可维护的代码。希望这些内容对你有所启发,欢迎交流与探讨!

相关推荐
noravinsc13 分钟前
‌HTTP 401错误
前端·javascript·vue.js
Magic夜灵21 分钟前
修复Electron项目Insecure Content-Security-Policy(内容安全策略CSP)警告的问题
前端·chrome·electron
二川bro26 分钟前
前端构建工具进化论:从Grunt到Turbopack的十年征程
前端·turbopack
A阳俊yi38 分钟前
SpringMVC响应页面及不同类型的数据,
java·前端·javascript
nyf_unknown1 小时前
(vue)elementUi中el-upload上传附件之后 点击附件可下载
javascript·vue.js·elementui
qq_332539451 小时前
React 前端框架推荐
前端·react.js·前端框架
拉不动的猪1 小时前
刷刷题34(uniapp中级实际项目问题-1)
前端·vue.js·面试
noravinsc1 小时前
electron获取鼠标在浏览器外的点击和拖拽等动作
javascript·electron·计算机外设
奔跑的露西ly2 小时前
【HarmonyOS NEXT】实现文字环绕动态文本效果
前端·javascript·html·harmonyos
irving同学462383 小时前
Next.js 组件开发最佳实践文档(TypeScript 版)
前端