nodejs promisify 实现
本文主要阐述了在 nodejs
中的 promisify
的实现方式,并归纳其核心原理。
本文以成文之时的
nodejs
的latest
版本 v14.8.0 为例。
是什么
回答 promisify 是什么
这个问题,本质上是在解决 promisify
可以为我们解决什么问题。在 node.js
v14.x 文档中,promisify
有以下定义:
const util = require('util')
const fs = require('fs')
const stat = util.promisify(fs.stat)
stat('.')
.then(stats => {
// Do something with `stats`
})
.catch(error => {
// Handle the error.
})
Takes a function following the common error-first callback style, i.e. taking an (err, value) => ... callback as the last argument, and returns a version that returns promises.
nodejs promisify
函数支持将所有形如支持 (err, value) => ...
形式回调函数的异步操作函数转化为一个始终返回 promise
实例的异步操作函数。
函数签名
在 TypeScript v3.9.7 的 @types/node
中存在多种函数重载签名,其中 常规签名 如下:
function promisify<TResult>(
fn: (callback: (err: any, result: TResult) => void) => void
): () => Promise<TResult>
在 没有指定自定义的 promisify 的行为时,将始终返回回调结果的 第一个 结果值。
// promisify 后将返回 value 值
(err, value) => void
// 在没有自定义 promisify 时,将 **仅返回** value0,而不返回 value1
(err, value0, value1) => void
源码实现
首先 debug
nodejs
源码存在至少两种可行方式:
-
根据引入的
package
在源码中,逐步向上找到目标源码; -
借助
editor
或IDE
自带断点功能和单步调试直接找到目标源码。
方式一需要额外的前置 nodejs
源码通识找到 package
的引入代码入口,故本文为了集中文章思想,将通过方式二阐述 promisify
的实现。并且在后文对断点的阐述其实可以反推方式一的推导过程。
首先定义以下 demo file
:
const { promisify } = require('util')
const fs = require('fs')
debugger
const readFile = promisify(fs.readFile)
在开启 VS Code
自动附着 功能后,在 VS Code
集成终端中输入:
node --inspect-brk <DEMO_FILE_NAME>.js
此时进入专有 Debug
模式,那么在 F11
单步调试的情况下,可进入 promisify
在当前 nodejs
版本下的实现(于 lib/internal/util.js):
const kCustomPromisifiedSymbol = Symbol('util.promisify.custom')
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs')
function promisify(original) {
if (typeof original !== 'function')
throw new ERR_INVALID_ARG_TYPE('original', 'Function', original)
if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol]
if (typeof fn !== 'function') {
throw new ERR_INVALID_ARG_TYPE('util.promisify.custom', 'Function', fn)
}
return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn,
enumerable: false,
writable: false,
configurable: true
})
}
// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol]
function fn(...args) {
return new Promise((resolve, reject) => {
original.call(this, ...args, (err, ...values) => {
if (err) {
return reject(err)
}
if (argumentNames !== undefined && values.length > 1) {
const obj = {}
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i]
resolve(obj)
} else {
resolve(values[0])
}
})
})
}
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original))
ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn,
enumerable: false,
writable: false,
configurable: true
})
return ObjectDefineProperties(fn, ObjectGetOwnPropertyDescriptors(original))
}
对于以上源码进行伪代码抽象,可以得到:
function promisify(original) {
// 保证 promisify 处理仅对函数生效
if (isNotFunction(original)) {
throw INVALID_ORIGINAL_TYPE_ERROR
}
// 边界情况处理,当传入的函数为已经被 promisify 的函数时,确保原 promisify 函数
// 及其特定字段仍然符合 promisify 后续的调用规则
if (isPromisifyFunction(original)) {
if (
isFunctionValueOfPromisifySpecialField(original[PROMISIFY_SYMBOL_FIELD])
) {
throw INVALID_PROMISIFY_FILED_VALUE
}
}
// 将传入的 original 函数 promisify 的关键处理逻辑,此时的 fn 即为 promisify 化
// 后的 original 结果函数
function fn(...args) {
// 始终返回一个 Promise 实例
return promisifyOriginal
}
// 重新定义 fn 的隐式原型,以保证能够通过 fn 调用 original 原型链上的原型方法
reReferenceFnPrototypeToOriginal(fn, original)
// 标识结果函数 fn 为 promisify 化处理过的函数,通过基本类型值 symbol 来确保
// 键名唯一
// 该特定属性保存的是 promisify 结果本身
definePromisifyFieldIntoFn(fn, promisifyField, descriptors)
// 重新定义 fn 的所有属性描述符回到 original 自身的属性描述符,此举为了保证
// promisify 化后的结果函数 fn 仍具有与 original 相同的各项属性及其属性描述
return reDefineAllDescriptorFnToDescriptor(fn, original)
}
经过以上抽象,不难看出 promisify
实现功能的核心在于其内部的 fn
函数:
// Names to create an object from in case the callback receives multiple
// arguments, e.g. ['bytesRead', 'buffer'] for fs.read.
const argumentNames = original[kCustomPromisifyArgsSymbol]
function fn(...args) {
return new Promise((resolve, reject) => {
original.call(this, ...args, (err, ...values) => {
if (err) {
return reject(err)
}
// 该 if 分支仅对自定义 promisify 函数生效
if (argumentNames !== undefined && values.length > 1) {
const obj = {}
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i]
resolve(obj)
} else {
resolve(values[0])
}
})
})
}
其核心功能在于:
-
始终返回一个
promise
实例; -
在
promise
实例化回调中,通过call
原型方法调用original
函数,并传入特定的回调函数; -
基于 2 的回调函数会在被调用时,
resolve
异步操作的结果或reject
异步操作失败的原因。值得注意的是,处理定义了自定义promisify
的symbol
(即kCustomPromisifyArgsSymbol
)的异步方法的回调会以object
的形式resolve
结果外,其他所有异步操作的结果仅支持 第一个回调结果值。
自定义 promisify
promisify
提供了自定义 promisify
功能,开发者可通过形如以下方式实现自定义 promisify
行为:
const util = require('util')
// 在 nodejs 运行时中,Symbol.for('nodejs.util.promisify.custom') 等价于下文
// util.promisify.custom
doSomething[util.promisify.custom] = foo => {
return getPromiseSomehow()
}
前文中我们忽略了存在 kCustomPromisifyArgsSymbol
定义的自定义的 promisify
的实现路径。本质上,不论是自定义的 promisify
还是 nodejs
内置默认的 promisify
行为其实现核心都是一致的,那么就是始终通过一个 promise
实例来包裹对应的异步操作,并在异步操作完成时,在回调函数内部 fulfilled
当前 promise
实例,进而实现基于 promise
的回调链路。他们的 不同之处 在于对回调函数实参的处理方式。
function fn(...args) {
return new Promise((resolve, reject) => {
original.call(this, ...args, (err, ...values) => {
// ...
if (argumentNames !== undefined && values.length > 1) {
const obj = {}
for (let i = 0; i < argumentNames.length; i++)
obj[argumentNames[i]] = values[i]
resolve(obj)
} // ...
})
})
}
从上文源码可见,对于自定义的 promisify
行为,在 fulfilled
返回的 promise
实例时,将 不再 直接传入原 (err, value) => any
中的第二结果参数了,转而将后续的所有参数通过 rest 参数
的形式接收,并通过 key-value
的形式赋值给 fulfilled
的 promise
结果实例。那么就是说在自定义 promisify
行为时,若存在多个结果参数时,将通过 object
返回给 then
回调。