Appearance
异步编程
事件循环 · 宏任务/微任务 · Promise · async/await · Generator · Node EventLoop
一、事件循环(Event Loop)
- JS 是单线程,异步任务分为宏任务和微任务
- 执行顺序:
- 执行主线程宏任务(调用栈)
- 遇到微任务 → 放入微任务队列
- 当前宏任务执行完毕,立即清空所有微任务
- 执行下一个宏任务(如 setTimeout、I/O、UI 渲染)
- 循环往复(Event Loop)
- 宏任务:setTimeout、setInterval、I/O、UI 渲染
- 微任务:Promise.then、MutationObserver、process.nextTick(Node)
Node.js 的 EventLoop(6 个阶段)
Node.js 与浏览器的 EventLoop 有很大差别:
┌───────────────────────┐
┌─>│ timers │ setTimeout/setInterval 回调
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks(异常) │ TCP ECONNREFUSED 等异常回调
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │ 空闲/预备状态(内部使用)
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ poll │ 轮询:执行 I/O 回调,检查定时器
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ check │ setImmediate 回调
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──│ close callbacks │ socket.destroy() 等 close 事件回调
└───────────────────────┘核心区别:
- 浏览器:微任务在每个相应的宏任务中执行
- Node.js:微任务在不同阶段之间执行
Node 版本差异:
- Node ≥ 11:与浏览器表现一致,一个定时器执行完立即执行相应微任务
- Node < 11:同类型定时器会连续执行完,再统一执行微任务
process.nextTick 的特殊性:
- 独立于 EventLoop 的任务队列
- 在每个 EventLoop 阶段完成后检查,优先于微任务执行
Node.js 异步非阻塞 I/O 原理
Node.js 异步 I/O 四大要素:EventLoop + I/O 观察者 + 请求对象 + 线程池
libuv 调用过程:
- 创建请求对象:创建 I/O 请求对象
req_wrap,将回调函数注入到oncomplete_sym属性 - 推入线程池:
QueueUserWorkItem()将请求对象推入线程池,JS 调用立即返回(异步达成) - 回调通知:线程池中 I/O 完成后,将结果存入请求对象,通过 IOCP 通知 EventLoop,执行回调
二、Promise
- 三种状态:pending(默认)、fulfilled(成功)、rejected(失败),状态一旦确认不可逆
- 核心 API:
.then(onFulfilled, onRejected)链式调用 - then 中抛出异常会传递给下一个 then 的 onRejected
Promise 消灭回调地狱三大手段
- 回调函数延迟绑定:回调不是在创建 Promise 时直接声明的,而是通过后面的
.then()传入 - 返回值穿透:then 中回调的返回值会作为新 Promise 的状态,可以链式传递
- 错误冒泡:中途的错误会一直向后传递,最终被
.catch()捕获,无需逐层处理
Promise 为什么要引入微任务?
三种可能的回调方式对比:
- 同步回调:阻塞整个脚本,CPU 利用率低,且无法实现延迟绑定
- 宏任务队列:若任务队列很长,回调迟迟得不到执行,实时性差
- 微任务(当前方案):放在当前宏任务最后执行,既异步又不延迟
Promise 采用微任务解决了两大痛点:异步回调避免 CPU 浪费 + 当前宏任务末尾执行保证实时性
Promise 链式调用核心实现
js
// then 方法核心逻辑(PENDING 状态)
MyPromise.prototype.then = function(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : error => { throw error }
return bridgePromise = new MyPromise((resolve, reject) => {
self.onFulfilledCallbacks.push((value) => {
try {
let x = onFulfilled(value)
resolvePromise(bridgePromise, x, resolve, reject) // 拆解 Promise
} catch(e) { reject(e) }
})
})
}
// 拆解 Promise:如果 then 回调返回的是 Promise,递归拆解直到非 Promise
function resolvePromise(bridgePromise, x, resolve, reject) {
if (x instanceof MyPromise) {
if (x.status === PENDING) {
x.then(y => resolvePromise(bridgePromise, y, resolve, reject), reject)
} else {
x.then(resolve, reject)
}
} else {
resolve(x)
}
}Promise.resolve / finally / all / race 实现
js
// Promise.resolve 三种情况
Promise.resolve = (param) => {
if (param instanceof Promise) return param // 传入 Promise 直接返回
return new Promise((resolve, reject) => {
if (param && param.then && typeof param.then === 'function') {
param.then(resolve, reject) // 传入 thenable 对象,跟随其状态
} else {
resolve(param) // 其他情况,直接 resolve
}
})
}
// finally:无论成功失败都执行,值原封不动往下传
Promise.prototype.finally = function(callback) {
this.then(value => {
return Promise.resolve(callback()).then(() => value)
}, error => {
return Promise.resolve(callback()).then(() => { throw error })
})
}
// Promise.all:全部成功才 resolve,任一失败即 reject
Promise.all = function(promises) {
return new Promise((resolve, reject) => {
let result = [], index = 0, len = promises.length
if (len === 0) { resolve(result); return }
for (let i = 0; i < len; i++) {
Promise.resolve(promises[i]).then(data => {
result[i] = data; index++
if (index === len) resolve(result)
}).catch(reject)
}
})
}
// Promise.race:第一个完成的直接决定最终状态
Promise.race = function(promises) {
return new Promise((resolve, reject) => {
for (let i = 0; i < promises.length; i++) {
Promise.resolve(promises[i]).then(resolve).catch(reject)
}
})
}手写 Promise 完整实现详见 手写 Promise
三、async / await
async:声明异步函数,自动返回 Promise,内部可使用 awaitawait:暂停异步执行,等待 Promise 完成并返回结果,只能在 async 内部使用- 本质是 Generator + 自动执行器的语法糖
async/await 执行顺序(经典面试题)
js
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() { console.log('async2') }
console.log('script start')
setTimeout(() => console.log('setTimeout'), 0)
async1()
new Promise(resolve => { console.log('promise1'); resolve() })
.then(() => console.log('promise2'))
console.log('script end')输出顺序:
script start → async1 start → async2 → promise1 → script end → promise2 → async1 end → setTimeout关键要点:
- Promise 链式 then():多个 then() 不是连续创建多个微任务,而是前一个 then() 执行后才将后一个 then() 的回调推入微任务队列
- await v 做了什么:await 后的值 v 会被转换为 Promise,await 后续代码类似传入 then() 的回调
Promise.resolve(v)≠new Promise(r => r(v)):若 v 是 Promise,前者直接返回 v,后者需要额外的 PromiseResolveThenableJob 处理
await 规范优化(Chrome 73+)
- 优化前(Chrome 71):
await v等价于new Promise(resolve => resolve(v)),会产生额外的微任务 - 优化后(Chrome 73+):
await v语义上等价于Promise.resolve(v),减少不必要的微任务层数
forEach 中 await 的问题
问题:forEach 无法保证异步操作按顺序执行
js
async function test() {
let arr = [4, 2, 1]
arr.forEach(async item => {
const res = await handle(item) // handle 返回 setTimeout 的 Promise
console.log(res)
})
console.log('结束')
}
// 实际输出:结束 → 1 → 2 → 4(不是期望的 4 → 2 → 1 → 结束)原因:forEach 底层是同步遍历立即执行回调,无法等待异步完成。
解决方案:使用 for...of(基于迭代器 Iterator)
js
async function test() {
let arr = [4, 2, 1]
for (const item of arr) {
const res = await handle(item)
console.log(res) // 按顺序输出 4, 2, 1
}
console.log('结束')
}四、Generator(生成器)与协程
Generator 基本语法:
- 带
*号的"函数"(实际不是函数),通过yield暂停和恢复执行 - 调用
gen()后不会立即执行,需调用next()才开始 next()返回{ value, done }
js
function* gen() {
console.log("enter")
let a = yield 1
let b = yield 2
return 3
}
var g = gen()
console.log(typeof g) // "object",不是 "function"
console.log(g.next()) // enter → { value: 1, done: false }
console.log(g.next()) // { value: 2, done: false }
console.log(g.next()) // { value: 3, done: true }协程(Coroutine):
- 比线程更轻量级的存在,处于线程之中,一个线程可有多个协程
- 不受操作系统管理,由应用程序代码控制(无上下文切换开销)
- Generator 就是协程的一种实现
Promise 版本:自动执行 Generator:
js
function run(g) {
const next = (data) => {
let res = g.next()
if (res.done) return
res.value.then(data => next(data))
}
next()
}手写 async/await 原理
- 本质是 Generator + 自动执行器的语法糖
- 利用 generator 分割代码片段,每个 yield 用 Promise 包裹
- 执行下一步的时机由 Promise 控制
js
function _asyncToGenerator(fn) {
return function() {
const self = this, args = arguments
return new Promise((resolve, reject) => {
const gen = fn.apply(self, args)
function _next(value) {
try {
const { value: val, done } = gen.next(value)
if (done) resolve(val)
else Promise.resolve(val).then(v => _next(v), e => {
try { gen.throw(e) } catch(err) { reject(err) }
})
} catch(e) { reject(e) }
}
_next(undefined)
})
}
}五、JS 异步编程方案演进
| 方案 | 特点 | 缺点 |
|---|---|---|
| 回调函数 | 最原始方式,高阶函数传入回调 | 回调地狱(多层嵌套)、错误处理混乱 |
| Promise | 链式调用、错误冒泡、延迟绑定 | 代码冗余,then 链过长 |
| Generator + co | 同步方式书写异步,协程控制流 | 需第三方库 co,上手成本高 |
| async/await | 终极方案,原生语法支持 | 需理解底层仍需掌握前三种 |
相关
- 手写 Promise — 符合 Promises/A+ 规范的完整实现
- 事件 — DOM 事件流、事件委托
- JS 引擎与执行上下文 — V8 引擎、执行上下文、闭包