
JavaScript 异步编程体系化知识总结
JavaScript 异步编程是语言核心特性之一,根植于单线程事件循环模型,历经四代范式演进,核心目标是解决异步流程的可读性、可维护性与并发控制问题。以下从底层机制、核心范式、并发控制、工程实践四个维度构建完整知识体系。
一、底层基石:单线程模型与事件循环
1.1 单线程与非阻塞 I/O
- 单线程设计本质:浏览器环境中 JavaScript 主线程与 DOM 渲染线程互斥,单线程可避免多线程操作 DOM 的竞态问题,保证线程安全。
- 非阻塞 I/O 原理:耗时操作(网络请求、定时器、文件读写)不阻塞主线程,通过回调/事件通知机制在完成后插入任务队列,由事件循环调度执行。
1.2 事件循环(Event Loop)核心机制
事件循环是异步编程的底层调度器,浏览器与 Node.js 实现略有差异,核心逻辑统一:
- 执行主线程同步代码(宏任务初始态)。
- 同步代码执行完毕,清空当前所有微任务队列。
- 取出一个宏任务执行,执行完成后再次清空所有微任务。
- 重复上述过程,形成循环。
| 任务类型 | 常见场景 |
|---|---|
| 宏任务(MacroTask) | script 整体代码、setTimeout/setInterval、I/O 操作、UI 渲染、setImmediate(Node) |
| 微任务(MicroTask) | Promise.then/catch/finally、MutationObserver、process.nextTick(Node)、queueMicrotask |
1.3 异步编程四代演进
- 回调函数时代:最原始的异步封装,可读性差。
- Promise 时代:ES6 标准化,解决回调地狱,支持链式调用与并发控制。
- Generator 时代:ES6 引入协程概念,实现异步代码同步化书写,需自动执行器。
- async/await 时代:ES2017 标准,Generator 的语法糖,异步编程终极方案。
二、核心异步范式与 API 详解
2.1 回调函数(Callback):原始异步方案
- 核心原理:将后续逻辑封装为函数,作为参数传入异步操作,操作完成后调用该函数。
- 典型风格:Node.js 错误优先回调(error-first callback),如
fs.readFile(path, (err, data) => {})。 - 核心缺陷:
- 回调地狱(Callback Hell):多层嵌套导致代码横向膨胀,可读性极差。
- 错误处理分散,难以统一捕获。
- 无法支持并发、取消、进度等高级控制。
2.2 Promise:异步编程标准化基石
Promise 是对异步操作的状态封装,通过状态机模型统一异步行为。
2.2.1 状态机模型
- 三种状态:
pending(进行中)、fulfilled(已成功)、rejected(已失败)。 - 状态不可逆:只能从
pending→fulfilled或pending→rejected,一旦变更永久固化。
2.2.2 实例方法
then(onFulfilled, onRejected):注册成功/失败回调,返回一个新的 Promise,支持链式调用。catch(onRejected):专门捕获失败状态,等价于.then(null, onRejected)。finally(onFinally):无论成功失败都会执行,不改变 Promise 状态,回调无参数。
2.2.3 静态方法
| 方法 | 行为逻辑 | 典型场景 |
|---|---|---|
Promise.resolve(value) |
快速创建成功 Promise;若 value 是 Promise 则直接返回 | 包装普通值为 Promise |
Promise.reject(reason) |
快速创建失败 Promise | 主动抛出异步错误 |
Promise.all(iterable) |
全部成功才成功,返回结果数组;任一失败则整体失败 | 多请求并行等待全部结果 |
Promise.race(iterable) |
第一个敲定的 Promise 状态即为整体状态 | 超时控制、最快结果返回 |
Promise.allSettled(iterable) |
等待所有任务敲定,返回每个任务的状态与结果 | 容错批量任务,需全量结果 |
Promise.any(iterable) |
第一个成功即成功;全部失败才抛出 AggregateError |
多源容错,只要一个成功即可 |
2.2.4 核心特性
- 链式调用与值穿透:
then返回新 Promise,回调返回普通值则传递给下一个then;返回 Promise 则等待其敲定。 - 错误穿透:链式调用中未被处理的错误会向后传递,直到遇到
catch。 - 立即执行性:Promise 构造函数内的代码同步执行,
then回调才是微任务。
2.2.5 局限性
- 一旦创建立即执行,无法中途取消。
pending状态无法获取进度信息。- 错误无法通过原生
try/catch捕获。
2.3 Generator:协程与可暂停的异步
Generator 是 ES6 引入的协程实现,支持函数执行的暂停与恢复,为异步同步化提供基础。
2.3.1 基础概念
- 迭代器协议:对象具有
next()方法,调用返回{value, done}结构。 - 生成器函数:通过
function*声明,内部使用yield表达式暂停执行,返回 Generator 对象(既是迭代器也是可迭代对象)。
2.3.2 核心方法
next(value):恢复执行,将value作为上一个yield的返回值,返回{value, done}。throw(error):向生成器内部抛出错误,可被内部try/catch捕获。return(value):强制终止生成器,返回{value, done: true}。yield*:委托另一个生成器,实现生成器嵌套调用。
2.3.3 异步应用
- 原理:用
yield暂停异步操作,异步完成后通过next传入结果,以同步写法描述异步流程。 - 自动执行器:Thunk 函数、
co库等工具可自动执行 Generator,自动处理 Promise 与回调。 - 定位:异步编程过渡方案,语法繁琐,需额外执行器,最终被 async/await 取代。
2.4 async/await:终极异步方案
async/await 是 ES2017 标准,本质是 Generator + Promise + 自动执行器 的语法糖。
2.4.1 语法定义
async function:声明异步函数,返回值永远是 Promise(普通返回值会被Promise.resolve包装)。await:只能在 async 函数内使用,等待 Promise 敲定,暂停当前函数执行,让出主线程;Promise 成功后恢复执行并返回结果。
2.4.2 执行机制
- 执行 async 函数,遇到
await立即暂停,将后续逻辑注册为微任务。 - 等待的 Promise 状态敲定后,恢复 async 函数执行。
- 函数执行完毕,返回的 Promise 状态随之敲定。
2.4.3 错误处理
- 优先使用
try/catch捕获await的失败,完全符合同步编程思维。 - 也可使用
await promise.catch(err => ...)行内捕获。 - async 函数内部未捕获的错误,会导致返回的 Promise 变为 rejected 状态。
2.4.4 注意事项
await只能在 async 函数作用域内使用,不能直接在普通回调(如forEach)中使用。- 无依赖的异步操作避免串行
await,应先收集 Promise 再用Promise.all并行。 - async 函数总是返回 Promise,即使无显式 return 也会返回
Promise.resolve(undefined)。
三、异步并发控制
3.1 核心概念区分
- 并发(Concurrency):单线程下多个任务在事件循环中交替执行,宏观上“同时进行”,微观上串行。
- 并行(Parallelism):多线程/多进程下多个任务真正同时执行;JS 本身不支持,需通过 Web Worker、Node.js 子进程实现。
3.2 基础并发模式(Promise 原生实现)
3.2.1 全成功并行:Promise.all
- 适用:多个无依赖任务同时发起,必须全部成功才能继续。
- 风险:“一损俱损”,任一任务失败会直接终止整体,其他任务结果被丢弃。
3.2.2 容错并行:Promise.allSettled
- 适用:需要所有任务的执行结果(无论成功失败)的批量场景。
- 返回:每个元素为
{status: 'fulfilled'|'rejected', value/reason}结构。
3.2.3 竞速模式:Promise.race
- 适用:超时控制、多接口取最快结果。
- 典型示例:请求超时中断
const withTimeout = (promise, ms) => Promise.race([promise, new Promise((_, reject) => setTimeout(() => reject('超时'), ms))]);
3.2.4 任一成功:Promise.any
- 适用:多源冗余请求,只要一个成功即可。
- 失败聚合:全部失败时抛出
AggregateError,包含所有失败原因。
3.3 高级并发控制
3.3.1 串行执行
- 适用:任务间有依赖,必须按顺序执行。
- 实现:
Array.reduce累加 Promise、for循环 +await。
3.3.2 限流并发(Concurrency Limit)
- 适用:海量异步任务,控制同时执行的数量(避免请求打爆服务器、耗尽连接池)。
- 原理:维护执行池与等待队列,执行中任务数小于阈值时,从等待队列取出任务执行。
- 工业级实现:
p-limit库;核心逻辑为信号量 + 任务队列。
3.3.3 批量分块并发
- 适用:超大量任务,按固定大小分批次执行,一批完成再执行下一批。
- 实现:数组分块 +
Promise.all批量处理。
3.3.4 异步取消与中断
- 标准方案:
AbortController+AbortSignal。 - 支持场景:
fetch、axios等原生/主流库均支持 signal 参数;自定义 Promise 可监听abort事件实现取消逻辑。
3.3.5 进度跟踪
- 适用:批量任务执行进度展示。
- 实现:维护计数器,每个任务完成时更新计数,结合
Promise.all等待全部完成。
四、工程实践与进阶
4.1 错误处理最佳实践
- Promise 链末尾必须添加
catch,避免未捕获的 rejection。 - async/await 优先使用
try/catch,核心独立异步操作可单独捕获。 - 全局兜底:监听
unhandledrejection事件,捕获所有未处理的 Promise 错误。
4.2 性能优化
- 并行优先:无依赖的异步操作一律使用
Promise.all并行,减少总耗时。 - 预加载/预请求:提前发起异步请求并缓存结果,用户触发时直接返回。
- 防抖节流:控制高频异步触发(搜索联想、滚动加载),避免无效请求。
4.3 经典手写实现
- 符合 Promise/A+ 规范的完整 Promise 实现。
- 手写
Promise.all、race、allSettled、any。 - 基于 Generator 实现简易 async/await(自动执行器)。
- 手写并发限流器(p-limit 核心逻辑)。
4.4 常见误区
- 循环中使用
await导致不必要的串行,应先收集 Promise 再await Promise.all。 - 误以为 Promise 在
then时才执行,实际构造函数内代码同步执行。 finally不接收参数,也不会改变 Promise 的结果状态。- async 函数内的错误不会阻塞主线程,但会导致返回的 Promise 失败。
五、体系总结与演进逻辑
JavaScript 异步编程的演进主线是可读性提升与控制能力增强:
- 从回调地狱的嵌套结构,到 Promise 的链式扁平结构。
- 从 Generator 的协程暂停能力,到 async/await 的同步化书写体验。
- 从基础的并发等待,到精细化的限流、取消、容错并发控制。
所有上层异步范式最终都基于事件循环 + 微任务队列的底层调度,掌握底层机制是理解所有异步行为的核心。日常开发优先使用 async/await 编写业务逻辑,复杂并发场景结合 Promise 静态方法与工具函数实现控制。