Skip to content

异步编程

事件循环 · 宏任务/微任务 · Promise · async/await · Generator · Node EventLoop


一、事件循环(Event Loop)

  • JS 是单线程,异步任务分为宏任务微任务
  • 执行顺序
    1. 执行主线程宏任务(调用栈)
    2. 遇到微任务 → 放入微任务队列
    3. 当前宏任务执行完毕,立即清空所有微任务
    4. 执行下一个宏任务(如 setTimeout、I/O、UI 渲染)
    5. 循环往复(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 调用过程

  1. 创建请求对象:创建 I/O 请求对象 req_wrap,将回调函数注入到 oncomplete_sym 属性
  2. 推入线程池QueueUserWorkItem() 将请求对象推入线程池,JS 调用立即返回(异步达成)
  3. 回调通知:线程池中 I/O 完成后,将结果存入请求对象,通过 IOCP 通知 EventLoop,执行回调

二、Promise

  • 三种状态:pending(默认)、fulfilled(成功)、rejected(失败),状态一旦确认不可逆
  • 核心 API.then(onFulfilled, onRejected) 链式调用
  • then 中抛出异常会传递给下一个 then 的 onRejected

Promise 消灭回调地狱三大手段

  1. 回调函数延迟绑定:回调不是在创建 Promise 时直接声明的,而是通过后面的 .then() 传入
  2. 返回值穿透:then 中回调的返回值会作为新 Promise 的状态,可以链式传递
  3. 错误冒泡:中途的错误会一直向后传递,最终被 .catch() 捕获,无需逐层处理

Promise 为什么要引入微任务?

三种可能的回调方式对比:

  1. 同步回调:阻塞整个脚本,CPU 利用率低,且无法实现延迟绑定
  2. 宏任务队列:若任务队列很长,回调迟迟得不到执行,实时性差
  3. 微任务(当前方案):放在当前宏任务最后执行,既异步又不延迟

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,内部可使用 await
  • await:暂停异步执行,等待 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

关键要点

  1. Promise 链式 then():多个 then() 不是连续创建多个微任务,而是前一个 then() 执行后才将后一个 then() 的回调推入微任务队列
  2. await v 做了什么:await 后的值 v 会被转换为 Promise,await 后续代码类似传入 then() 的回调
  3. 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终极方案,原生语法支持需理解底层仍需掌握前三种

相关