第14期 | promisify
学习目标:
(1)promisify可以实现什么功能?
(2)promisify和Promise有什么关系?
一、初识promisify
remote-git-tags用于读取指定git仓库的tags和对应的hash值。index.js代码不多,主要用到了node中两个方法,一个是util中的promisify,一个是child_process的execFile。
1、引入promisify和childProcess
2、包裹childProcess.execFile方法
3、执行包裹后的execFile方法
const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);可以理解成在git命令行窗口输入这样一行命令:
git ls-remote --tags https://github.com/sinndresorhus/got,得到的结果如图所示:
4、解析返回的数据
5、返回结果
import { promisify } from 'node:util';
import childProcess from 'node:child_process';
// 将 childProcess.execFile 用 promisify 包裹起来
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) {
// 等待 包裹了一层 promisify 的 execFile 执行成功,并返回结果
const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);
const tags = new Map();
// 对返回的数据进行解析处理
for (const line of stdout.trim().split('\n')) {
const [hash, tagReference] = line.split('\t');
const tagName = tagReference.replace(/^refs\/tags\//, ''),replace(/\^{}$/, '');
tags.set(tagName, hash);
}
return tags;
}
二、execFile方法
如果想要从remote-git-tags的index.js中了解promisify可以实现的功能,首先需要了解execFile是如何使用的。
通过查阅API,结合上面的例子得知,execFile用于执行特定的程序,将参数以数组的形式传入,然后用回调函数将结果返回。举例如下:
import childProcess from 'node:child_process';
childProcess.execFile('git', ['ls-remote', '--tags', 'https://github.com/sinndresorhus/got'], (error, stdout, stderr) => {
console.log(stdout);
})
import childProcess from 'node:child_process';
childProcess.execFile('node', ['--version'], (error, stdout, stderr) => {
console.log(stdout);
})
接下来回到remote-git-tags中查看包裹了一层的execFile方法是如何使用的:
const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);
是不是有种似曾相识的感觉?没错,这里用到了ES6中的await语法。用于等待某个异步方法返回结果之后,再继续执行后面的操作。大胆猜想一下,promisify方法是不是将原本需要在回调函数中获取的内容,改成了通过Promise返回。
三、promisify源码
以下是util promisify源码。
const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
let validateFunction;
function promisify(original) {
// Lazy-load to avoid a circular dependency.
if (validateFunction === undefined)
({ validateFunction } = require('internal/validators'));
validateFunction(original, 'original');
if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];
validateFunction(fn, 'util.promisify.custom');
return ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
__proto__: null,
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) => {
ArrayPrototypePush(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]);
}
});
ReflectApply(original, this, args);
});
}
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
__proto__: null,
value: fn, enumerable: false, writable: false, configurable: true
});
const descriptors = ObjectGetOwnPropertyDescriptors(original);
const propertiesValues = ObjectValues(descriptors);
for (let i = 0; i < propertiesValues.length; i++) {
// We want to use null-prototype objects to not rely on globally mutable
// %Object.prototype%.
ObjectSetPrototypeOf(propertiesValues[i], null);
}
return ObjectDefineProperties(fn, descriptors);
}
promisify.custom = kCustomPromisifiedSymbol;
1、引入自带的校验方法
校验传入的参数是否是一个function
const validateFunction = hideStackFrames(value, name) => {
if (typeof value !== 'function') {
throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
}
}
2、判断original[kCustomPromisifiedSymbol]是否存在
如果存在,则校验original[kCustomPromisifiedSymbol]是否是一个function,然后在对象上定义一个新属性,并返回。
3、定义一个方法fn
const argumentNames = original[kCustomPromisifyArgsSymbol];
function fn(...args) {
return new Promise((resolve, reject) => {
ArrayPrototypePush(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]);
}
});
ReflectApply(original, this, args);
});
}
4、设置一个指定对象的原型到另一个对象
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
然后在对象上定义一个新属性:
ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
__proto__: null,
value: fn, enumerable: false, writable: false, configurable: true
});
5、获取对象所有自身属性的描述符,将值放入fn,并返回
const descriptors = ObjectGetOwnPropertyDescriptors(original);
const propertiesValues = ObjectValues(descriptors);
for (let i = 0; i < propertiesValues.length; i++) {
// We want to use null-prototype objects to not rely on globally mutable
// %Object.prototype%.
ObjectSetPrototypeOf(propertiesValues[i], null);
}
return ObjectDefineProperties(fn, descriptors);
四、将回调函数改造成Promise
1、loadData.js
export function loadData([param1, param2, param3], cb) {
console.log('222')
setTimeout(() => {
console.log('333')
let a = [
'loadData',
param1,
param2,
param3
]
cb(a)
}, 2000)
}
2、promisify.js
export function promisify(oldFunc) {
function fn(...args) {
return new Promise((resolve, reject) => {
args.push((...values) => {
console.log('444')
resolve(values)
})
console.log('111')
Reflect.apply(oldFunc, this, args);
})
}
return fn
}
3、test.js
import { loadData } from './demo/loadData.js'
import { promisify } from './demo/promisify.js'
const loadDataPromise = promisify(loadData)
async function testLoad() {
const content = await loadDataPromise(['111', '222', '333'])
console.log('testLoad', content)
}
testLoad();
五、收获
1、语法
1.1 Symbol.for()
1.2 Object.setPrototypeOf()
设置一个指定的对象的原型(即,内部 [[Prototype]] 属性)到另一个对象或 null。
1.3 Object.getOwnPropertyDescriptors()
用来获取一个对象的所有自身属性的描述符。
1.4 Object.defineProperty()
1.5 Reflect
2、promisify
(1)promisify可以实现将回调函数改造成Promise返回。
(2)promisify中用到了Promise语法,以此实现对回调函数的改造。