1.说明
本文参加了由公众号@若川视野 发起的每周源码共读活动,点击了解详情一起参与。
2.源码
为了方便直接使用,此处直接拉取若川针对remote-git-tags
项目的分析的项目remote-git-tags-analysis
。
//拉取项目
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
//进入到项目目录
cd remote-git-tags
// 安装依赖
npm i
2.1调试
为了能进行调试,我们需要打上断点,如下图,index.js
是入口文件,test.js
是测试文件;
在package.json
文件中找到script
脚本命令区域,如下图,我们需要启动项目,方法有多种(注意由于我默认保存会格式化代码,与xo自带eslint冲突,因此我把test中xo删除了,原项目的test命令与下图的dev一样),下面罗列了几种:
- 终端执行npm run test;
或
- 鼠标放到test脚本命令上,会悬浮提示执行【运行脚本 | 调试脚本】,点击调试脚本;
启动项目后,就会停留在我们打的断点处,就可以进行调试。
2.2代码分析
我们先看下入口文件index.js
。
完整代码:
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]);
console.log(stdout); //stdout输出信息如下,后面根据\n换行符和\t制表符分割
// 67316ee37f0028d660f530970ab74a571247ff10 refs/tags/v0.1.0
// 1e7746d8c44a75ee9ec8ed94356dd5e8ba9c3bc2 refs/tags/v0.1.0^{}
// ......
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;
}
- 导入promisify: promisify是node中用于把异步的回调函数转换成promise的形式的方法,后面我们会讲讲promisify的实现。
import { promisify } from "node:util";
- 导入childProcess: childProcess是node中用于创建子进程的方法,详情可看childProcess。 并利用promisify对异步执行子进程命令的过程转成promise对象。
import childProcess from "node:child_process";
const execFile = promisify(childProcess.execFile);
- 封装获取标签的方法:remoteGitTags
export default async function remoteGitTags(repoUrl) {
// 设置执行子进程的命令+ 参数:git ls-remote --tags repoUrl
const { stdout } = await execFile("git", ["ls-remote", "--tags", repoUrl]);
// stdout格式如下:
// 67316ee37f0028d660f530970ab74a571247ff10 refs/tags/v0.1.0
// 1e7746d8c44a75ee9ec8ed94356dd5e8ba9c3bc2 refs/tags/v0.1.0^{}
const tags = new Map();
for (const line of stdout.trim().split("\n")) {
const [hash, tagReference] = line.split("\t");
// hash: 67316ee37f0028d660f530970ab74a571247ff10
// tagReference: refs/tags/v0.1.0 或 refs/tags/v0.1.0^{}
const tagName = tagReference
.replace(/^refs\/tags\//, "")
.replace(/\^{}$/, "");
// tagName: v0.1.0
tags.set(tagName, hash);
}
return tags;
}
分析:根据传入的url执行子进程,拉取所有的tags版本信息,然后对stdout数据进行\n和\t的截取,以及replace的替换,获取到所有的版本信息存储在map对象中。
- test.js文件中调用
remoteGitTags('https://github.com/sindresorhus/got')
,完成整个过程。
3.promisify的实现
promisify
函数是把 异步callback
形式转成 promise
形式,我们知道 Node.js
天生异步
,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。
- 简单的异步回调:
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';
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}
loadImage(imageSrc, function(err, content){
if(err){
console.log(err); return;
}
console.log(content);
});
上面简单的实现了异步加载图片的过程,通过封装loadImage
函数,实现图片加载完成后或者失败时,进行回调。
- promise的优化:
const loadImagePromise = function(src){
return new Promise(function(resolve, reject){
loadImage(src, function (err, image) {
if(err){
reject(err);
return;
}
resolve(image);
});
});
};
loadImagePromise(imageSrc).then(res => {
console.log(res);
}).catch(err => {
console.log(err);
});
我们把1中的回调函数形式加载图片的方式,改造成了promise
的形式。
但是仅仅是这样,功能就太单一,这个转换promise
形式的被局限在转换图片加载了,我们需要封装一个通用化的函数,接收函数参数,该函数参数是各种异步回调函数。
- 通用的promisify函数:
function promisify(original){
function fn(...args){
return new Promise((resolve, reject) => {
// 之前有提过nodejs天生异步,执行异步时错误/成功会执行回调函数,因此此处手动给参数
// 最后追加一个带有err和value的回调函数,方便node自动回调
args.push((err, ...values) => {
if(err){
return reject(err);
}
resolve(values);
});
// 此处使用显示改变this,是为了避免original内部使用了this,而定制化promise
//对象过程中又手动改了this指向,进而导致this错误,此处始终把original指向定制化函数
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}
//柯里化定制需求函数:
const loadImagePromise = promisify(loadImage); //此处可以根据需求确定是否显示改变this
async function load(){
try{
const res = await loadImagePromise(imageSrc);
console.log(res);
} catch(err){
console.log(err);
}
}
load();
以上便是手动封装自定义的promisify
的过程,当然node中promisify
的源码肯定对各边界情况进行多重定义,并且使用了内部各种方法等,因此会和上面我们封装的简易过程不一致,但本质逻辑和思路是一样,下面附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;