remote-git-tags与回调函数Promise化实现

240 阅读1分钟

本文参加了由公众号@若川视野 发起的每周源码共读活动,   点击了解详情一起参与。

这是源码共读的第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"));

image.png

获取远程仓库下所有标签 并格式化为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;
}

总结和思考

  1. promiseify将回调函数 转为promise的 过程实现
  2. 通常学习一个知识点的时候,还需要思考它的作用和场景,当你的知识储备量起来后, 很多知识点就可以串联起来
  3. 学好Node是非常必要的!