本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
在实际开发中把某方法回调函数的形式promise化可以说是很常用的手段了,比如开发微信小程序使用的wx.request如果使用promise封装就更好用。今天要学习的是node中promisyfy,它就是把callback函数转成promise形式的好能手!
1.学习资料
参考资料:川哥的文章
从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现
2.分析源码
首先看一个蛮有用的库:remote-git-tags,作用是获取远程仓库的所有标签,核心源码如下:
import {promisify} from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) {
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');
// Strip off the indicator of dereferenced tags so we can override the
// previous entry which points at the tag hash and not the commit hash
// `refs/tags/v9.6.0^{}` → `v9.6.0`
const tagName = tagReference.replace(/^refs/tags//, '').replace(/^{}$/, '');
tags.set(tagName, hash);
}
return tags;
}
这段代码的主要流程就是:
(1)通过 ls-remote --tag 命令获取所有的tag, 并输出到标准输出中
(2)遍历标准输出的每一行,提取tag的名字和hash值
(3)以名字为键,hash为值存在map中,最后返回map
这里面的亮点是:
import {promisify} from 'node:util';
import childProcess from 'node:child_process';
const execFile = promisify(childProcess.execFile);
这里面有promisify方法,是把callback异步方法转成promise的形式;还有child_process中的execFile方法,可以执行命令。
本次详细学习promisify方法, 其实在之前的玩具vite中就这样引入过代码:
const readFile = require('util').promisify(fs.readFile)
等价形式为:
import {promisify} from 'node:util';
const readFile = promisify(fs.readFile)
思考一下promisify的实现思路:
1.return 一个promise
2.在new Promise中包装了原来的异步函数,并且成功回调中resolve,失败回调reject,形如:
new Promise ((resolve,reject) => {
异步函数({
success: function() {resolve},
failed: function() {reject}
})
})
在川哥的代码中给出一步一步实现一个通用的promisify的过程:
// 一张青柠
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';
function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.alt = '公众号若川视野专用图?';
image.style = 'width: 200px;height: 200px';
// 加载成功给callback传参数(null, 图片dom)
image.onload = () => callback(null, image);
// 加载失败给callback传参('加载失败')
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}
在第一个html中展示的是回调的方法:
<body>
<script src="./loadImage.js"></script>
<script>
// 调用loadImage方法, 回调函数中判断有没有错误。。。
loadImage(imageSrc, function (err, image) {
if (err) {
console.log(err);
return;
}
console.log(image);
});
</script>
</body>
第二个html中展示了对loadImage方法promise化的过程:
<body>
<script src="./loadImage.js"></script>
<script>
// 接受一个参数:图片地址
const loadImagePromise = function(src){
// 返回一个promise
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
// 加载失败reject
reject(err);
return;
}
// 加载成功resolve
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
})
.catch(err => {
console.log(err);
});
</script>
</body>
当然后面的调用处也可以改成async await的形式:
async function load(){
const res = await loadImagePromise(imageSrc)
console.log(res)
}
load()
第三个html展示了更加通用的promise化的方法:
<body>
<script src="./loadImage.js"></script>
<script>
function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}
const loadImagePromise = promisify(loadImage);
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
}
catch(err){
console.log(err);
}
}
load();
</script>
</body>
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, {
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, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}
3.调试代码
打好断点:
启动调试:
小插曲:
报错:提示文件的引入方式不正确(模块化)
对策:卸载node, 安装最新稳定版
4.收获和总结
1.node util 中 promisify的原理
2.remote-git-tags实现原理
3.child_process中的execFile方法的使用
学习完本期源码,您可以思考如下问题:
1.package.json中type字段的含义?
2.package.json中engines字段的含义?
3.git中使用什么命令查看远程仓库的tags?
4.如何引用node原生库?
5.是否了解node中child_process的execFile方法?
6.简述remote-git-tags的原理?
参考资料: