Skip to content

Node.js 异步编程

Node.js 的高性能源于其异步非阻塞模型。理解回调、Promise、async/await 以及事件循环的执行顺序,是写好 Node.js 的关键。

作者:yanshaodong

Callback(回调)

最早期的异步写法,将函数作为参数传入,操作完成后调用。

javascript
const fs = require('fs')

fs.readFile('file.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err)
    return
  }
  console.log(data)
})

回调地狱(Callback Hell):多层嵌套导致代码难以维护。

javascript
fs.readFile('a', (err, a) => {
  fs.readFile('b', (err, b) => {
    fs.readFile('c', (err, c) => {
      // 嵌套越来越深
    })
  }
})

Promise

Promise 把"未来的值"封装为对象,用 .then / .catch 链式处理,解决嵌套问题。

javascript
const fs = require('fs').promises

fs.readFile('a.txt', 'utf8')
  .then((a) => fs.readFile('b.txt', 'utf8'))
  .then((b) => console.log(b))
  .catch((err) => console.error(err))

三种状态:pending(进行中)→ fulfilled(成功)/ rejected(失败)。

async / await

基于 Promise 的语法糖,让异步代码看起来像同步代码。

javascript
async function readAll() {
  try {
    const a = await fs.readFile('a.txt', 'utf8')
    const b = await fs.readFile('b.txt', 'utf8')
    return a + b
  } catch (err) {
    console.error('读取失败', err)
  }
}

并发优化:无依赖的异步操作应并行,而非串行 await。

javascript
// 串行(慢):a 完成才读 b
const a = await read('a')
const b = await read('b')

// 并行(快):同时发起
const [a, b] = await Promise.all([read('a'), read('b')])

事件循环与执行顺序

Node.js 事件循环分为多个阶段,理解微任务与宏任务的区别至关重要。

  • 微任务(Microtask)Promise.then / queueMicrotask / process.nextTick(优先级最高)。
  • 宏任务(Macrotask)setTimeout / setInterval / setImmediate / I/O 回调。
javascript
console.log('1')                        // 同步

setTimeout(() => console.log('2'), 0)   // 宏任务

Promise.resolve().then(() => console.log('3')) // 微任务

process.nextTick(() => console.log('4'))        // 微任务(先于 Promise)

console.log('5')                        // 同步
// 输出:1, 5, 4, 3, 2

事件循环阶段(简化)

  1. timerssetTimeout / setInterval 回调
  2. pending callbacks:推迟的 I/O 回调
  3. idle, prepare:内部使用
  4. poll:检索新的 I/O 事件
  5. checksetImmediate 回调
  6. close callbacks:关闭事件

每轮循环结束后,先清空所有微任务(含 nextTick 队列),再进入下一阶段。

错误处理

javascript
// 1. async 函数内的 try/catch
try {
  await riskyOp()
} catch (err) {
  // 处理
}

// 2. Promise 链统一 catch
doAsync().catch(handleError)

// 3. 进程级兜底(避免崩溃)
process.on('uncaughtException', (err) => {
  console.error('未捕获异常', err)
})
process.on('unhandledRejection', (reason) => {
  console.error('未处理的 Promise 拒绝', reason)
})

生产环境务必监听 uncaughtExceptionunhandledRejection,并配合进程管理器(PM2 / Docker restart)实现自愈。

常见陷阱

  • 忘记 awaitconst data = fetchData() 拿到的是 Promise 而非结果。
  • 在循环里 await:应改用 Promise.all 并行。
  • 同步阻塞:在事件循环里执行重计算会卡住所有请求,应移入 worker_threads

作者:yanshaodong