{T}

回调函数

回调(Callback)是 Node.js 中最基本的异步处理方式。Node.js 通过事件循环调度 I/O 操作,大量 API 采用回调模式在 I/O 完成时通知用户代码。

Node.js 的错误优先回调约定

Node.js 采用"错误优先回调"(Error-First Callback)约定:回调函数的第一个参数固定用来传递错误对象,后续参数才是成功结果。

javascript
const fs = require("fs")

fs.readFile("config.json", "utf8", (err, data) => {
  if (err) {
    console.error("读取文件失败:", err)
    return
  }
  console.log("文件内容:", data)
})

约定规则:

  • err !== null 表示操作失败,调用方应尽早返回
  • err === null 表示成功,此时才能安全地使用后续结果

这一约定让调用者可以在一个回调中同时处理成功与失败,避免遗漏错误分支。

Node.js 中常见的回调场景

文件系统操作

javascript
const fs = require("fs")

// 读取文件
fs.readFile("data.txt", "utf8", (err, data) => {
  if (err) throw err
  console.log(data)
})

// 写入文件
fs.writeFile("output.txt", "Hello Node.js", (err) => {
  if (err) throw err
  console.log("写入完成")
})

// 获取文件信息
fs.stat("data.txt", (err, stats) => {
  if (err) throw err
  console.log("文件大小:", stats.size)
})

网络请求

javascript
const http = require("http")

http.get("http://example.com", (res) => {
  let data = ""
  res.on("data", (chunk) => {
    data += chunk
  })
  res.on("end", () => {
    console.log("响应内容:", data)
  })
}).on("error", (err) => {
  console.error("请求失败:", err.message)
})

数据库操作

javascript
// MySQL 示例
connection.query("SELECT * FROM users", (err, rows) => {
  if (err) {
    console.error("查询失败:", err)
    return
  }
  console.log("查询结果:", rows)
})

// Redis 示例
client.get("user:123", (err, reply) => {
  if (err) {
    console.error("获取失败:", err)
    return
  }
  console.log("缓存数据:", reply)
})

事件监听

javascript
const EventEmitter = require("events")
const emitter = new EventEmitter()

emitter.on("data", (data) => {
  console.log("收到数据:", data)
})

emitter.emit("data", { message: "Hello" })

回调地狱问题

当多个异步任务需要顺序执行时,回调往往产生层层嵌套:

javascript
fs.readFile("config.json", "utf8", (err, content) => {
  if (err) return console.error(err)
  parseConfig(content, (err, config) => {
    if (err) return console.error(err)
    connect(config.db, (err, connection) => {
      if (err) return console.error(err)
      connection.query("SELECT * FROM users", (err, rows) => {
        if (err) return console.error(err)
        console.log(rows)
      })
    })
  })
})

回调地狱的问题:

  • 可读性差,逻辑难以追踪
  • 错误处理分散且容易遗漏
  • 难以复用与测试
  • 控制权交给第三方回调,发生异常时排查困难

解决方案

方案一:拆分并命名回调

将匿名函数提炼成具名函数,按步骤拆分逻辑:

javascript
function handleRead(err, content) {
  if (err) return console.error(err)
  parseConfig(content, handleParse)
}

function handleParse(err, config) {
  if (err) return console.error(err)
  connect(config.db, handleConnect)
}

function handleConnect(err, connection) {
  if (err) return console.error(err)
  connection.query("SELECT * FROM users", handleQuery)
}

function handleQuery(err, rows) {
  if (err) return console.error(err)
  console.log(rows)
}

fs.readFile("config.json", "utf8", handleRead)

方案二:使用 util.promisify 转换

util.promisify 是 Node.js 提供的工具函数,可以将错误优先回调风格的函数转换为 Promise 风格:

javascript
const util = require("util")
const fs = require("fs")

// 转换单个函数
const readFile = util.promisify(fs.readFile)

readFile("config.json", "utf8")
  .then(data => console.log(data))
  .catch(err => console.error(err))

// 配合 async/await 使用
async function loadConfig() {
  try {
    const data = await readFile("config.json", "utf8")
    return JSON.parse(data)
  } catch (err) {
    console.error("加载配置失败:", err)
    throw err
  }
}

方案三:使用 fs/promises(推荐)

Node.js 10+ 提供了原生的 Promise 风格 API:

javascript
const fs = require("fs/promises")

async function loadUsers() {
  try {
    const raw = await fs.readFile("config.json", "utf8")
    const config = JSON.parse(raw)
    const connection = await connectAsync(config.db)
    const rows = await queryAsync(connection, "SELECT * FROM users")
    console.log(rows)
  } catch (error) {
    console.error("加载用户失败:", error)
  }
}

自定义 promisify 行为

对于非标准回调签名,可以自定义 promisify 实现:

javascript
const util = require("util")

// 自定义回调签名:(result) 而不是 (error, result)
function customCallbackApi(data, callback) {
  setTimeout(() => callback(data.toUpperCase()), 100)
}

// 自定义 promisify 实现
customCallbackApi[util.promisify.custom] = (data) => {
  return new Promise((resolve) => {
    customCallbackApi(data, resolve)
  })
}

const promisifiedCustom = util.promisify(customCallbackApi)
const result = await promisifiedCustom("hello") // 'HELLO'

Node.js 特有的定时器 API

Node.js 提供了浏览器没有的定时器 API:

process.nextTick

在当前操作完成后立即执行,优先级最高:

javascript
process.nextTick(() => {
  console.log("nextTick 回调")
})

console.log("同步代码")

// 输出顺序:
// 同步代码
// nextTick 回调

setImmediate

在下一个事件循环周期执行:

javascript
setImmediate(() => {
  console.log("setImmediate 回调")
})

setImmediate vs setTimeout(fn, 0):

javascript
const fs = require("fs")

// 在 I/O 回调中,setImmediate 总是先于 setTimeout 执行
fs.readFile(__filename, () => {
  setTimeout(() => console.log("setTimeout"), 0)
  setImmediate(() => console.log("setImmediate"))
})
// 输出顺序:setImmediate → setTimeout

最佳实践

1. 始终处理错误

javascript
// ❌ 错误:未处理错误
fs.readFile("data.txt", "utf8", (err, data) => {
  console.log(data)
})

// ✅ 正确:处理错误
fs.readFile("data.txt", "utf8", (err, data) => {
  if (err) {
    console.error("读取失败:", err)
    return
  }
  console.log(data)
})

2. 优先使用 Promise API

javascript
// ❌ 旧风格:回调
fs.readFile("data.txt", "utf8", (err, data) => { /* ... */ })

// ✅ 新风格:Promise
const fs = require("fs/promises")
const data = await fs.readFile("data.txt", "utf8")

3. 避免嵌套过深

javascript
// ❌ 嵌套过深
asyncOp1((err, result1) => {
  asyncOp2(result1, (err, result2) => {
    asyncOp3(result2, (err, result3) => {
      // ...
    })
  })
})

// ✅ 使用 Promise 链或 async/await
const result1 = await asyncOp1()
const result2 = await asyncOp2(result1)
const result3 = await asyncOp3(result2)

4. 合理使用 process.nextTick

javascript
// ✅ 使用 nextTick 确保回调始终异步执行
function readFileCached(filename, callback) {
  if (cache.has(filename)) {
    process.nextTick(() => callback(null, cache.get(filename)))
  } else {
    fs.readFile(filename, callback)
  }
}

// ❌ 避免 nextTick 递归
function bad() {
  process.nextTick(bad) // 会阻塞事件循环
}

小结

  • Node.js 采用"错误优先回调"约定
  • 回调地狱可以通过拆分函数或使用 Promise 解决
  • util.promisify 可以将回调函数转换为 Promise
  • 优先使用 fs/promises 等 Promise 风格的原生 API
  • process.nextTicksetImmediate 是 Node.js 特有的异步 API
  • 始终处理回调中的错误,避免遗漏异常