尽管 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 的值,它也会被隐式地包装成一个 Promise。await 关键字后面必须是一个 Promise。它会暂停函数的执行,直到 Promise 完成,然后返回 Promise 的结果。
async/await 是一种包装好的 Promise 的语法糖。它让异步代码写起来更像同步代码,同时保留了 Promise 的强大功能。
2. 异步任务并发常用方法
在 JS/TS 中,处理并发任务时,Promise 提供了多个实用的方法,包括 Promise.all、Promise.allSettled、Promise.race 和 Promise.any。它们各自有不同的适用场景和行为特点。
2.1 Promise.all
2.1.1 Promise.all特性
- 适用于 多个异步任务并行执行,所有任务完成后返回所有成功的结果。
- 如果 任意一个 Promise 失败,则整个 Promise.all 立即失败(短路机制)。
- 返回值是 一个数组,包含所有 Promise 的 value,但如果有一个 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.allSettled:Promise.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 结束。
- 每个任务的结果都会返回,包括 fulfilled 和 rejected 状态。
- 返回值是 一个数组,每个元素是包含 status 和 value(成功)或 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 操作(如网络请求、文件读取、数据库操作等)时,合理管理和释放资源至关重要。系统梳理核心知识点,能帮助我们更深入理解和高效使用异步并发操作,从而编写出更加健壮、可维护的代码。希望这些内容对你有所启发,欢迎交流与探讨!