本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第14期 | promisify
本次将弄明白回调函数变为Promise的过程,以及对于remote-git-tags这个库的实现思路分析
remote-git-tags
Get tags from a remote Git repo
从远程仓库获取所有标签, 可以用来查看仓库下的所有标签,方便我们进行切换或者查阅
仓库
git clone https://github.com/sindresorhus/remote-git-tags.git
使用
import remoteGitTags from "remote-git-tags";
console.log(await remoteGitTags("https://github.com/lxchuan12/blog.git"));
获取远程仓库下所有标签 并格式化为Map显示
源码分解
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();
console.log(stdout)
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;
}
ESM模块
在NodeV13.2后增加了对于ESM模块的默认支持,我们只需要在package.json中声明type
字段
"type":"module"
node: prefix
引用Node原生库 支持以node:prefix
的形式,例如
import url from 'node:util'
Git获取标签命令
库的实现本质就是 调用Git相关的command
获取结果 并存储到Map中
git ls-remote --tags repoUrl
child_process
调用关键,Node下的子进程模块,childProcess.execFile
, 以不产生shell的情况下去执行 command
,它主要执行可执行的文件或者名称
child_process.execFile(file[, args][, options][, callback])
const { execFile } = require('child_process');
const child = execFile('node', ['--version'], (error, stdout, stderr) => {
//stdout 返回的可读流结果
if (error) {
throw error;
}
console.log(stdout);
});
Reflect
它主要用来拦截JS操作的方法, Reflect所有方法和属性都是静态,有些方法和Object相同
Reflect.apply
通过指定的参数列表 去触发目标函数的调用
promisify原理
顾名思义,是一个Promise化的函数,它主要的作用就是将回调函数 改为Promise的形式去调用
编写一个例子来体会它的变化
//普通调用,通过回调的形式去判断结果
import childProcess from "node:child_process";
const child = (command, callback) => {
childProcess.execFile(command, ["--version"], callback);
};
child("node", (err, stdout) => {
if (err) {
console.log(err);
return;
}
console.log(stdout);
});
import childProcess from "node:child_process";
//回调转Promise函数
const promiseify = (fn) => {
//返回一个新函数 将用来接收 要调用的命令
return function (...args) {
//函数执行后会返回一个Promise,Promise执行内部给形参增加了回调函数
return new Promise((resolve, reject) => {
args.push((err, stdout) => {
//判断结果
if (err) return reject(err);
resolve(stdout);
});
//调用一次fn函数 并传入参数以及this绑定
Reflect.apply(fn, this, args);
});
};
};
const child = (command, callback) => {
childProcess.execFile(command, ["--version"], callback);
};
const promiseifyFile = promiseify(child);
console.log(await promiseifyFile("node"));
直观上来看,好像第一个例子 代码比起第二个要少很多,但抛开promiseify
函数不看,会发现第二个例子的调用反而更简洁,它主要的好处就是可以通过Promise的形式去调用,我们还可以通过 async await
语法去获得结果
Promiseify将返回一个新的函数, 新函数仅接受一个参数传入,用来决定要调用的命令, 函数内部最终返回一个Promise
,并且在函数内部给函数的形参push
了一个回调函数进去, 在回调中通过判断err和stdout
决定resolve 和 reject的结果,最后在Promise的内部调用Reflect.apply
,手动触发了这个函数的调用并完整传入参数
to
结合上期优雅的await捕获错误(不清楚原理的小伙伴可以看我上期的内容),此时我们可以将代码改造成这样
import childProcess from "node:child_process";
const child = (command, callback) => {
childProcess.execFile(command, ["--version"], callback);
};
const promiseify = (fn) => {
return function (...args) {
return new Promise((resolve, reject) => {
args.push((err, stdout) => {
if (err) rejct(err);
resolve(stdout);
});
Reflect.apply(fn, this, args);
});
};
};
const to = (promise, errorExt) => {
return promise
.then((res) => [null, res])
.catch((err) => {
if (errorExt) {
let parsedError = Object.assign({}, err, errorExt);
return [parsedError, undefined];
}
return [err, undefined];
});
};
const promiseifyFile = promiseify(child);
const res = await to(promiseifyFile("node"));
console.log(res);
//[ null, 'v16.13.2\r\n' ]
此时我们就可以通过 数组下标的形式 去判断 我们函数的调用是成功 还是失败了, 此时我们是结合了promiseify
以及to
函数后,直接通过数组下标来判断结果了
源码其他部分解析
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]);
//它的输出结果 是这种形式 中间一个制表符, 结尾换行符
//260fb118a36a980624539cdb786beb4cf440ef43 refs/tags/3.0.0
//c584493e8ee0cc691e90bb8a8b0535de88e1617d refs/tags/3.0.0^{}
//cb914f81b0968a50c4782091c9773cfea1f1123b refs/tags/3.0.1
const tags = new Map();
//以 \n 分割 stdout 的文本内容,转为一个数组 那么就变成了这样
['260fb118a36a980624539cdb786beb4cf440ef43 refs/tags/3.0.0',
'c584493e8ee0cc691e90bb8a8b0535de88e1617d refs/tags/3.0.0^{}']
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`
//这里的也有作用的注释 就是正则替换
//先将 refs/tags/ 替换为空, 再替换尾部的^{}
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
//将结果增加进Map中
tags.set(tagName, hash);
}
return tags;
}
总结和思考
- promiseify将回调函数 转为promise的 过程实现
- 通常学习一个知识点的时候,还需要思考它的作用和场景,当你的知识储备量起来后, 很多知识点就可以串联起来
- 学好Node是非常必要的!