如何手写 promisify?Node 风格回调如何转 Promise?
简化版
promisify 用来把 Node 风格的回调函数转换成返回 Promise 的函数。Node 风格回调通常是 (err, data) => {},如果 err 存在就 reject,否则 resolve 结果。实现时要保留 this、透传参数、处理多结果场景,并确保回调只结算一次。
详细版
最基础实现是返回一个新函数,调用原函数时追加回调。
| 细节 | 说明 |
|---|---|
| 错误优先 | 第一个回调参数是错误 |
| this | 用 fn.call(this, ...) 保留调用者 |
| 参数 | 原函数参数原样透传 |
| 多结果 | 可 resolve 数组或只取第一个结果 |
function promisify<T = unknown>(fn: Function) {
return function (this: unknown, ...args: unknown[]) {
return new Promise<T>((resolve, reject) => {
fn.call(this, ...args, (err: unknown, data: T) => {
if (err) reject(err)
else resolve(data)
})
})
}
}
这题常见坑是忘记保留
this,导致对象方法被包装后内部上下文丢失。
完整版教学
一、Node 风格回调是什么
Node 早期大量 API 使用错误优先回调,也就是:
fs.readFile('a.txt', 'utf8', (err, data) => {
if (err) return console.error(err)
console.log(data)
})
第一个参数表示错误,后面的参数表示成功结果。
二、promisify 的核心思路
Promise 的结算方式是 resolve 或 reject。所以 promisify 的本质是搭一座桥:回调收到错误就 reject,收到成功结果就 resolve。
function promisify(fn: Function) {
return (...args: unknown[]) =>
new Promise((resolve, reject) => {
fn(...args, (err: unknown, result: unknown) => {
err ? reject(err) : resolve(result)
})
})
}
这是最小版本,但面试里还要补上下文和边界。
三、为什么要保留 this
如果原函数是对象方法,它内部可能使用 this。
const obj = {
prefix: 'hi',
get(cb: Function) {
cb(null, this.prefix)
}
}
包装后如果直接 fn(...args),this 会丢。更稳的写法是返回普通函数,并使用 fn.call(this, ...)。
四、处理多个成功结果
有些回调不是只有一个成功值,例如 (err, stdout, stderr)。可以选择只 resolve 第一个,也可以提供 multiArgs 选项。
function promisifyMulti(fn: Function) {
return function (this: unknown, ...args: unknown[]) {
return new Promise((resolve, reject) => {
fn.call(this, ...args, (err: unknown, ...values: unknown[]) => {
if (err) reject(err)
else resolve(values.length > 1 ? values : values[0])
})
})
}
}
五、回调多次调用怎么办
Promise 只能结算一次。即使原函数错误地多次调用回调,后续的 resolve/reject 也不会改变状态。
不过工程上仍可加一个 called 标记,避免执行额外逻辑。
六、和 util.promisify 的关系
Node 内置 util.promisify 已经提供成熟实现,还支持自定义 util.promisify.custom。面试手写是为了考察你是否理解回调与 Promise 的转换机制。
| 能力 | 手写基础版 | Node 内置版 |
|---|---|---|
| 错误优先回调 | 支持 | 支持 |
| this 处理 | 取决于实现 | 调用方式仍需注意 |
| 自定义转换 | 通常无 | 支持 |
| 类型完善 | 简单 | 更成熟 |
七、常见误区与追问
- 误区:promisify 可以包装任意回调。 它默认适合错误优先回调,不适合事件监听或多次触发的回调。
- 误区:箭头函数返回包装函数更方便。 箭头函数没有自己的
this,包装对象方法时容易丢上下文。 - 误区:回调多次调用会让 Promise 多次 resolve。 Promise 状态只会第一次结算。
- 追问:如何处理多个成功参数? 可以 resolve 数组,或提供配置决定只取第一个。
- 追问:事件监听能 promisify 吗? 单次事件可以封装成 Promise,多次事件更适合 Observable 或 EventEmitter。
- 追问:如何保留 this? 返回普通函数,并用
fn.call(this, ...args, callback)。
八、加强记忆
promisify 记成“回调出口改成 Promise 出口”。错误优先回调里有 err 就 reject,没有 err 就 resolve;真正拉开差距的是 this、多参数和不适用场景。