持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
这是源码共读的第2期,链接:juejin.cn/post/708315…
前言
如何将一个callback转换为promise,在学习remote-git-tags这个库的时候,看到其引用了node原生模块的promisify,因此一探究竟。
remote-git-tags库源码解析
该库的作用是:获取远程仓库的所有tag信息。
其原理就是:git ls-remote --tags repoUrl(远程仓库地址)
看下其源码实现:
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;
}
代码包含注释总共22行,很精简的代码,下面分析下其主要流程:
- 首先通过
git ls-remote --tasg命令,获取所有的tag。 - 其次将获取到的tag字符串,以
\n分割为数组,并遍历,提取每一个tag的名字和hash值。 - 最后,以tagName为键,hash为值存入到生产的map中,并返回该map。
node的模块机制
在源码中,我们看到其引入的模块方式是ES6的模块机制,import {promisify} from 'node:util';
在Node 13之前,Node都是CommonJS的模块机制,在Node 13中添加了对标准ES6模块的支持。因此,我们在本库中,看到其引入node模块都是通过import from.
如何告知node加载什么模块:
一种方式,就是改变文件的扩展名。如果是.mjs结尾的文件,则Node就会将其作为ES6模块来加载。如果是.cjs结尾的文件,则Node将其作为CommonJS模块加载。
另一种方式就是在package.json文件中,设置type字段。属性值为module则使用ES6模块。值为commonjs或者为空的话就默认以commonjs模块加载。
node的promisify方法
其原理就是将异步方法中最后一个参数是回调函数,且回调函数中有两个参数:error和data,将该callback promise化。
看下其使用:
// 使用前
fs.readFile('xx/xxx.json', (err, data) => {})
// 使用promisify后
const readFilePromise = promisify(fs.readFile)
readFilePromise('./index.js')
.then(data => {})
.catch(err => {})
实现一个promisify方法
function promisify(original) {
function fn(...args) {
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if(err) {
return reject(err);
}
resolve(values);
})
Reflect.apply(original, this, args);
})
}
return fn
}
- 返回一个函数,该函数会将传入来的异步方法用promise来包裹。
args也就是传入的异步函数,其他的非回调函数的参数。Reflect.apply()。在内部将回调函数合入传入的参数数组内,利用apply来执行,将参数传入其中。Reflect.apply就相当Function.prototype.apply.call(func, thisArg, args)。
node模块的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, {
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)
);
}
promisify.custom = kCustomPromisifiedSymbol;
相对与上面我们自己实现的方法,node的方法更健壮,考虑了更多的情况,加了对应的校验逻辑,但其核心原理是跟上面一致的。
收获
- node的两种模块机制,这块在之前不太了解,本次源码学习扩展了node这块的知识面。
- promisiy的实现原理,将具备一定通用规则的异步方法的callback prmoise化
- Reflect,学习了ES6中为操作对象提供的新的API
思考
这里实现的promisify方法只能使最后一个参数是回调函数,且具备两个参数error和data promise化。只有这种规则的异步方法才可以用此方法。但我们了解其原理后,就可以利用这种方式将我们项目中的具备一定规则的异步方法promise化。