相互独立的任务非常适合并发执行。尽管 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
的值,它也会被隐式地包装成一个 Promise
。await
关键字后面必须是一个 Promise
。它会暂停函数的执行,直到 Promise
完成,然后返回 Promise
的结果。
说得更直白点,async/await
是一种包装好的 Promise
的语法糖。它让异步代码写起来更像同步代码,同时保留了 Promise
的强大功能。
2 异步任务并发常用方法
在 JS/TS 中,处理并发任务时,Promise
提供了多个实用的方法,包括 Promise.all
、Promise.allSettled
、Promise.race
和 Promise.any
。它们各自有不同的适用场景和行为特点。
2.2 Promise.all
2.2.1 特性
-
适用于 多个异步任务并行执行,所有任务完成后返回所有成功的结果。
-
如果 任意一个
Promise
失败 ,则整个Promise.all
立即失败(短路机制)。 -
返回值是 一个数组 ,包含所有
Promise
的value
,但如果有一个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
结束。 -
每个任务的结果都会返回,包括
fulfilled
和rejected
状态。 -
返回值是 一个数组 ,每个元素是包含
status
和value
(成功)或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
清理定时器
- 任务竞争 :
Promise.race()
会返回第一个完成的 Promise,无论其是fulfilled
还是rejected
。 - 及时中止 :使用
AbortController
监听任务状态,及时清理多余的异步任务,防止资源泄漏。 - 避免多次触发:确保在任务完成或失败后,其他未完成的任务被中止。
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 副作用消除
异步任务如果不加管理,容易引入副作用。例如,未及时取消的网络请求可能导致数据污染 或资源浪费。
并发请求时清理未完成任务
- 防止数据竞争:当一个任务成功时,其他未完成的任务会被中止,避免多个请求竞争修改数据。
- 任务粒度控制:及时终止不再需要的异步操作,降低系统负担。
- 兼顾成功与失败:不论任务成功还是失败,均确保释放资源,防止资源浪费。
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 操作(如网络请求、文件读取、数据库操作等)时,合理管理和释放资源至关重要。系统梳理核心知识点,能帮助我们更深入理解和高效使用异步并发操作,从而编写出更加健壮、可维护的代码。希望这些内容对你有所启发,欢迎交流与探讨!