领先的免费Web技术教程,涵盖HTML到ASP.NET

网站首页 > 知识剖析 正文

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

nixiaole 2025-04-08 17:14:26 知识剖析 12 ℃

尽管 JavaScript 和 TypeScript 是单线程的,但在事件循环(Event Loop)机制的加持下,异步任务的并发处理能力依然非常强大。本文将深入探讨 JS/TS 中常用的异步任务并发核心知识点。


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

1.1 Promise

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

1.2 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.1 Promise.all

2.1.1 Promise.all特性

  • 适用于 多个异步任务并行执行,所有任务完成后返回所有成功的结果。
  • 如果 任意一个 Promise 失败,则整个 Promise.all 立即失败(短路机制)。
  • 返回值是 一个数组,包含所有 Promisevalue,但如果有一个 Promise 失败,则直接抛出该错误。
  • 不会阻止已开始执行的 Promise,即使某个任务已失败,其他任务仍会继续执行。
  • 只要有一个 Promise 失败,整个 Promise.all 立即 reject,但未完成的任务不会被中断,需手动清理可能存在的副作用(如定时器、网络请求等)。
"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.1.2 Promise.all适用场景/最佳实践

  • 所有任务必须成功的场景:适用于需要 所有异步任务都成功 才能继续下一步的业务逻辑。例如,批量数据请求、多个依赖任务的并行执行。
  • 提升性能,避免等待顺序执行:适用于 多个独立、无依赖 的异步操作,使用 Promise.all 可以 并行执行,提升执行效率。
  • 批量处理、批量校验:适用于需要同时执行 多个异步验证,并在全部验证通过后统一处理结果。
  • 多接口数据聚合:适用于从 多个数据源 获取数据并在返回后进行统一处理。
  • 执行时间监控:可与 Promise.race 结合使用,实现超时控制,确保并行任务在规定时间内完成,否则抛出错误。


2.1.3 在Promise.all中子Promise优雅抛出异常

  • 方式1:将 Promise.all 替换为 Promise.allSettledPromise.allSettled 不会因某个任务失败而中断,适合需要全面了解每个任务执行结果的场景。
"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));


  • 方式2:对子 Promise 进行 catch 包装:即使某个任务失败,也不会影响整体任务的执行。
"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));


  • 方式3:给 Promise 失败提供默认值:为失败的任务提供默认值,避免整体任务失败。
"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));


  • 方式4:使用 async/await 手动处理异常:通过 try...catch 捕获异常,确保任务失败时不会中断其他任务。
async function runTasks() {
    try {
        const results = await Promise.all([p1, p2, p3]);
        console.log("所有任务成功", results);
    }
    catch (error) {
        console.log("某个任务失败", error);
    }
}

await runTasks()


2.2 Promise.allSettled

2.2.1 Promise.allSettled特性

  • 不会因某个任务失败而中断,会等待所有 Promise 结束。
  • 每个任务的结果都会返回,包括 fulfilledrejected 状态。
  • 返回值是 一个数组,每个元素是包含 statusvalue(成功)或 reason(失败)的对象。
  • 所有任务都会执行,无论成功与否,适合需要全面了解每个任务执行结果的场景。

2.2.2 Promise.allSettled适用场景/最佳实践

  • 监控多个异步任务的执行情况,不关心个别任务是否失败,所有任务的执行结果都需要收集并处理的场景。
  • 资源清理:无论任务成功或失败,都需要执行清理操作。
  • 日志记录:适用于需要记录所有异步任务执行情况的需求。

2.2.3 Promise.allSettled代码示例

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.3 Promise.race

2.3.1 Promise.race特性

  • 谁先完成(成功或失败) ,就以该 Promise 的结果为准,其他未完成的任务会被忽略。
  • 返回值是第一个完成的 Promise 结果(不论 fulfilled 还是 rejected)。
  • 适用于竞态(Race Condition)场景,只需要第一个完成的结果,忽略其他任务。
  • 不会取消未完成的 Promise,如果有副作用,需手动清理(如定时器、网络请求等)。

2.3.2 Promise.race适用场景/最佳实践

  • 超时控制:与 Promise 结合,设置超时机制,若某任务未及时完成,则用超时结果替代。
  • 快速响应:多个数据源竞争,谁先返回就采用谁的结果(如多CDN)。
  • 资源竞争:适用于同时尝试多个资源,获取最快的可用结果。

2.3.3 Promise.race代码示例

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.4 Promise.any

2.4.1 Promise.any特性

  • 第一个成功的 Promise 就会返回其结果,忽略失败的任务。
  • 如果所有 Promise 都失败,返回一个 AggregateError,包含所有错误信息。
  • 返回值是第一个 fulfilled 的结果,与 Promise.race 不同,Promise.any 忽略 rejected
  • 不会取消未完成的 Promise,如果有副作用,需手动清理。

2.4.2 Promise.any适用场景/最佳实践

  • 容错处理:在多个异步任务中,只要有一个成功,就能继续执行后续逻辑。
  • 备选方案:尝试多个操作,只要有一个成功,就采用其结果(如多个 API 请求)。
  • 主备切换:主任务失败时,使用备选任务的结果作为兜底方案。

2.4.3 Promise.any代码示例

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.any([p1,p2,p3])
    .then((res) => console.log("任务成功:",res))
    .catch((err)=>console.log("任务失败:",err))


3. 异步并发任务规避踩坑

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

3.1 资源释放:使用 AbortController清理定时器

  • 任务竞争Promise.race() 会返回第一个完成的 Promise,无论其是 fulfilled 还是 rejected
  • 及时中止:使用 AbortController 监听任务状态,及时清理多余的异步任务,防止资源泄漏。
  • 避免多次触发:确保在任务完成或失败后,其他未完成的任务被中止。
// 示例代码
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 副作用消除

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

  • 防止数据竞争:当一个任务成功时,其他未完成的任务会被中止,避免多个请求竞争修改数据。
  • 任务粒度控制:及时终止不再需要的异步操作,降低系统负担。
  • 兼顾成功与失败:不论任务成功还是失败,均确保释放资源,防止资源浪费。
// 代码示例
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 常见坑及规避策略

3.3.1 未捕获的异常

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

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


3.3.2 竞态条件(Race Condition)

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

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


3.3.3 内存泄漏

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

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


3.3.4 异步任务顺序依赖

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

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


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



Tags:

最近发表
标签列表