持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第1天,点击查看活动详情
1. 引言
最近学习热情不够之前高涨,趁着官方的六月更文活动,积极学习,努力产出,正向刺激自己不断提升自我,加油!!!ヾ(◍°∇°◍)ノ゙
2. 准备工作
2.1 拉取源码
并不是源码阅读的第一步,但本地阅读源码体验确实会比在github上直接看体验要好些,当然只是个人感受。这里我使用的是若川大佬提供的代码仓库github,
3. 关于Node
Node13后添加对标准ES6模块的支持。可以使用不同的文件后缀名来告知Node加载的是什么模块,如.mjs表示作为ES6模块来加载,.cjs表示作为CommonJS模块来加载;此外对于.js结尾的文件,默认情况下是CommonJS模块,若同级目录及所有项目目录存在package.json 文件,可在package.json文件中配置type属性为module表示使用ES6模块。
// package.json
{
// 指定 Node 以什么类型模块加载,缺省为 commonjs
"type": "module"
}
4. 源码解析
项目就一个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]);
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;
}
remoteGitTags
-
首先看
remoteGitTags函数所完成的工作,函数接受一个参数repoUrl标识仓库的url,支持远程仓库;await execFile('git', ['ls-remote', '--tags', repoUrl]);,获取到shell命令git ls-remote --tags repoUrl的执行结果,暨获取到指定远程仓库的tags列表。执行下git ls-remote --tags https://github.com/vuejs/core.git,可以看到返回结果如下(截取部分):d8c1536ead56429f21233bf1fe984ceb3e273fe9 refs/tags/v3.0.0 7d2ae08277b448fd1ce2ef9ba18f854d0dcd27d4 refs/tags/v3.0.0-alpha.0 1bb1271b5e78cc4b446ce363de73e68db74a6c89 refs/tags/v3.0.0-alpha.1 de81faf00ab22c68ae2a0df14c9e28547f35f544 refs/tags/v3.0.0-alpha.10 7402951d945b4e49474661594992a95f878de3f0 refs/tags/v3.0.0-alpha.11 1d9f8fc979a1c62e381a89eb26d8659a2c527f09 refs/tags/v3.0.0-alpha.12 11654a6e50839519b67efc571b696abd13a4d180 refs/tags/v3.0.0-alpha.13 -
之后是遍历版本和
Hash放到一个Map对象中,并作为一个返回值返回for (const line of stdout.trim().split('\n'))遍历取出每一行的数据const [hash, tagReference] = line.split('\t');取出hash值和tagReferenceconst tagName = tagReference.replace(/^refs/tags//, '').replace(/^{}$/, '');使用 两次替换,最终获得版本
5. promisify
可以注意到在代码的一开始有一个全局定义
const execFile = promisify(childProcess.execFile);
其中promisify函数将callback转化为promise,也是全文的重点。
3.1 回调函数
图片加载
const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png'; // 定义默认图片
function loadImage(src, callback) {
const image = document.createElement('img');
image.src = src; // 设置src属性
image.style = 'width: 200px;height: 200px'; // 设置样式属性
image.onload = () => callback(null, image);
image.onerror = () => callback(new Error('加载出错'));
document.body.append(image); // 将DOM元素添加到body中
}
- 定义图片加载函数,接收两个参数,一个是图片地址,一个是图片回调函数
document.createElement('img')创建新的DOM元素callback(null, image)设置onload事件时调用callback函数,onload是<img>元素事件,当图片加载完成时触发callback(new Error('加载出错'))设置onerror事件时调用callback函数,onerror也是<img>中的事件,当图片加载出错时触发
回调函数事件处理
loadImage(imageSrc, function (err, image) {
if (err) { // 出错时,打印错误信息
console.log(err);
return;
}
console.log(image); // 成功时打印<img>元素
});
调试时使用了若川大佬提供的代码库,所以选择examples/index-1.html文件,然后直接在VSCode中运行,简单步骤如下
-
选择需要调试运行的
html文件,选择运行与调试: -
选择运行的浏览器环境:
-
VSCode会自动打开所选择的浏览器执行 -
在
VSCode中设置的断点也会在浏览器中效
3.2 promise
可以浅试一下将上面的回调方式转化为promise的方式
const loadImagePromise = function (src) {
return new Promise(function (resolve, reject) {
loadImage(src, function (err, image) {
if (err) {
reject(err); // 使用reject返回错误,可被.catch方法捕获到
}
resolve(image); // 使用resolve返回结果,可被.then方法获取到
});
});
};
loadImagePromise(imageSrc)
.then((data) => {
console.log(data);
})
.catch((err) => {
console.error(err);
});
对于每一个回调函数都可以使用这种方式转化为Promise,但是出于逻辑复用的考虑,一般会将上面转化修改为更加通用的方式,promisify函数就是这样的一个通用方法。
我们可以先尝试修改上述代码为通用的方式,以便于理解promisify原理
const promisify = function (func) {
return function (...args) {
return new Promise(function (resolve, reject) {
func.call(this, args, function (err, ...values) {
if (err) {
reject(err);
}
resolve(values);
});
});
};
};
基本原理其实并不难,promisify本质上就是一个返回值为函数的函数,并且返回的函数将回调方式转化为Promise方式,但这里存在一点小问题,就是如果使用fun.call()的话虽然在这个例子中能正常运行,但当需要传递多个参数时会失效。
看了下若川大佬的实现
function promisify(original) {
function fn(...args) {
return new Promise((resolve, reject) => {
args.push((err, ...values) => {
if (err) {
return reject(err);
}
resolve(values);
});
// original.apply(this, args);
Reflect.apply(original, this, args);
});
}
return fn;
}
后知后觉我可以使用apply呀,不就可以传参数数组了嘛,优化下之前的代码👇
const promisify = function (func) {
return function (...args) {
return new Promise(function (resolve, reject) {
args.push(function (err, ...values) {
if (err) {
reject(err);
}
resolve(values);
});
func.apply(this, args);
});
};
};
顺便还学到了一个新的对象Reflect,Reflect对象是一个内置对象(不可以被构造出,不需要使用new创建),提供了一系列拦截JavaScript操作的方法,可以简单理解为以比较非常规的手段操作对象,这里只简单说下代码中出现的Reflect.apply()方法,其余更多函数可参考MDN
Reflect.apply
静态方法,通过指定参数列表发起对目标函数的调用,和apply方法类似,但是该方法接受三个参数
function需要调用的目标函数(比apply多的一个参数)this函数调用时绑定的this对象args函数调用时传入的实参列表,类数组对象
6. 总结
通过本次源码阅读也获得了以下几点收获
- 了解到
node的模块加载机制 - 学到一个新的
git命令git ls-remote --tags repoUrl - 学到了如何将回调函数异步调用转化为
Promise方式,以及如何进一步封装为更具有普适性的promisify函数 - 巩固了
call()和apply()两个方法的异同 - 学到一个新的调用函数的方法
Reflect.apply
继续加油呀👍!!!
7. 参考文章
《从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现》
\