回调函数
回调(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.nextTick和setImmediate是 Node.js 特有的异步 API- 始终处理回调中的错误,避免遗漏异常