前言
本文参加了由公众号@若川视野 发起的每周源码共读活动。从简单到进阶学习源码中的巧妙之处,旨在于将学习的东西应用到实际的开发中,同时借鉴源码中的思想,规范自我开发,以及锻炼自己的开发思维。也欢迎大家加入大佬的源码共读活动。一起卷起来。
正文
我们经常会在本地git仓库切换tags,或者git仓库切换tags,但是有谁真正的是去了解这其中的过程呢,我猜没有人,接下来我来带领你们去熟悉一下remote-git-tags
这个获取tags的源码库,这个个源码库的总共22行代码。
学习目标
- 1.熟悉获取git仓库所有tags的原理
- 2.学会调试源码
- 3 学会node中的promisify的原理和实现
- 4.学习其他的javascript的知识
remote-git-tags是如何使用的
import remoteGitTags from 'remote-git-tags'
console.log(await remoteGitTags('https://github.com/jiakaiqiang/mymangment'))
//=> Map {'testrags' => 'c39343e7e81d898150191d744efbdfe6df395119'}
源码调试
克隆源码
//克隆大佬的项目
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git
# npm i -g yarn //没有安装yarn环境的可以全局安装一下环境
cd remote-git-tags && yarn //切换到remote-git-tags 目录下同时执行yarn 安装依赖
调试
用VSCO的打开在pageage.json中scripts 中找到test 命令 鼠标放在test命令中 选择调试命令,会进入调试模式。
pageage.json讲解
{
//运行文件的模式是以commonjs模式还是esModules模式
"type":"module",
//运行的文件入口
"exports":"./index.js",
engines": {
//运行时的node环境
"node": "^12.20.0 || ^14.13.1 || >=16.0.0"
},
},
"scripts": {
//运行的指令
"test": "xo && ava"
},
我们都知道之前的node一直以commonjs规范运行的 ,在node13 的时候添加了对于es6模块的支持,对于文件后缀是.cjs
的则以commonjs
的方式去运行,对于.mjs
的文件则以esModules
规范运行,对于js文件则默认
是以commonjs
的规范,如果同级有pageage.json文件存在的话则按照指定的type类型运行 ,type的值分为module和commonjs两种
。
源码分析
// index.js
//引入异步执行函数
import {promisify} from 'node:util';
//引入子线程模块
import childProcess from 'node:child_process';
//将execFile 执行函数转成异步执行函数 execFile 是什么 ,他是命令执行函数,可理解成是cmd的执行窗口的函数替换.
const execFile = promisify(childProcess.execFile);
export default async function remoteGitTags(repoUrl) { //参数则是我们获取的仓库地址
//execFile 的参数有两个以第一个是 执行的指令 git npm java 等 第二个是数组的形式 则代表的是 执行的命令参数 返回值中的stdout 则是成功后的值 还有stderr
const {stdout} = await execFile('git', ['ls-remote', '--tags', repoUrl]);
//创建一个map 对象存放 tagsname 和对应的hash
const tags = new Map();
for (const line of stdout.trim().split('\n')) {
// 获取到hash 和tagPath
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`
//将tagPath 转成纯的tagName
const tagName = tagReference.replace(/^refs\/tags\//, '').replace(/\^{}$/, '');
//每个tagName 设置对应的hash
tags.set(tagName, hash);
}
return tags;
}
源码很简单
通过执行 git ls-remote --tags https://github.com/vuejs/vue-next.git
我们可以看到获取的服务器上的所有tags
那源码部分我们看完了 接下来我们认识一下node中的异步函数
promisify
我们都知道在node.js是异步的 ,那他是怎样实现异步呢,他是通过promisify()将回调函数转成promise,接下来我们按照 promisify()实现方式自己先实现一下
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';
function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src;
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载失败'));
document.body.append(image);
}
基础实现
//promisify将回调函数转成promise
我们可以实现
function promisify(src){
return new Primise((resolve,reject)=>{
loadImage(src,function(error,result){
if(error){
reject(error)
}else{
resolve(result)
}
})
})
}
promisify(imageSrc).then(res=>{
console.log(res) //图片的信息
}).catch(err=>{
console.log(err) //加载失败
})
上面实现了异步加载图片的需求,但是有会有人问,那如果加载的不是图片的其他的呢?欧 那这种方式就不行了 太局限了,我们编写一下通用的
通用实现
function promisify(callback) {
return function (...args) {
new Primise((resolve, reject) => {
args.push((err, ...values) => {
if (err) {
rejct(err)
} else {
resolve(values)
}
})
Reflect.apply(callback, this, args) ///通过这种方式去调用callback 传入我们的数组参数 args对于目前的场景来说 第一参数就是图片途径,后面的参数这是图片加载的执行函数
})
}
}
const loadImagePromise = promisify(loadImage);
async function load() {
try {
const res = await loadImagePromise(imageSrc);
console.log(res);
} catch (err) {
console.log(err);
}
}
load();
上述是我们根据promisify描述自己实现的,那我接下来看一下utils中的promisify的源码实现,其实和我们实现的基本一样、
定义util.promisify.custom和customPromosifyArgs的唯一标识
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'));
///验证orginal 是不是方法
validateFunction(original, 'original');
//判断是否是一自定义的promise函数
if (original[kCustomPromisifiedSymbol]) {
const fn = original[kCustomPromisifiedSymbol];
///验证fn 是否是方法
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.
//将自定义的promise函数赋值
const argumentNames = original[kCustomPromisifyArgsSymbol];
function fn(...args) {
return new Promise((resolve, reject) => {
/// 返回函数的参数和callback添加在一起 后执行promisify的参数函数
ArrayPrototypePush(args, (err, ...values) => {
if (err) {
return reject(err);
}
如果自定义的promise 函数存在 并且返回值长度大于1 则按照自定义promise和返回值创建一一对应关系,
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);
});
}
//将fn的原型设置成original的原型
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
value: fn, enumerable: false, writable: false, configurable: true
});
return ObjectDefineProperties(
fn,
ObjectGetOwnPropertyDescriptors(original)
);
}
promisify.custom = kCustomPromisifiedSymbol;
总结
遇到问题
下载代码执行yarn后我们进行调试的时候 vscode报如下错误:
vue : 无法加载文件 C:\Program Files\nodejs\vue.ps1,因为在此系统上禁止运行脚本。有关详细信息,请参阅 https:/go.microsoft
.com/fwlink/?LinkID=135170 中的 about_Execution_Policies。
所在位置 行:1 字符: 1
问题原因:计算机禁止了不安全脚本的执行。
解决方法:
以管理员的身份
运行PowerShell ,输入 set-executionpolicy remotesigned
选择Y 即可
收获
1.remot-git-tags原理,通过nodejs子线程中的execFile方法执行git ls-remote --tags repoUrl 获取所有的tags和对应的hash 将其存放在map 中返回。 2.promisify实现异步的原理,通过闭包的方式将创建的new Promise返回 在promise中执行 传入的回调函数,将结果取出。 3.ReflectApply()和apply的区别:
- 参数 ReflectApply()的参数有三个 执行函数,this指向,执行函数的参数 这三个值都不能进行省略 且第三个参数必须是数组 apply参数有两个 this指向,执行函数参数,第二个参数可以省略,不传则获取的都是undefined,但在实践中第一个参数也可以不传 不传的话默认指向window
- 异常处理 apply在异常抛出的语义上不明确,直接报错出.call不是一个函数.
Function.prototype.apply.call(null) // Thrown:
// TypeError: Function.prototype.apply.call is not a function Function.prototype.apply.call(null,console)
// Thrown: // TypeError: Function.prototype.apply.call is not a function Function.prototype.apply.call(null,console.log)
///- 输出为空,符合预期
如果将参数补齐了 同样是有问题的参数 则报错又是另外一种情况
Function.prototype.apply.call(console, null, [])
// Thrown:
// TypeError: Function.prototype.apply was called on #<Object>, which is a object and not a function
Function.prototype.apply.call([], null, [])
// Thrown:
// TypeError: Function.prototype.apply was called on [object Array], which is a object and not a function
Function.prototype.apply.call('', null, [])
// Thrown:
// TypeError: Function.prototype.apply was called on , which is a string and not a function
Reflect.apply() 如果只传入一个不可调用的对象 报出的异常则是
Reflect.apply(console) // Thrown: // TypeError: Function.prototype.apply was called on #<Object>, which is a object and not a function
如果我们传递正确的可调用函数,才会去校验第三个参数,也就是说它的参数校验是有顺序的
**Reflect.apply(console.log)
// Thrown:
// TypeError: CreateListFromArrayLike called on non-object**
3.形成了 断点调试 +源码主线+ 知识点补充 + 总结的源码学习方式