《若川视野x源码共读》第14期 源码学习系列之Nodejs中promisify原理和实现

153 阅读5分钟

前言

本文参加了由公众号@若川视野 发起的每周源码共读活动。从简单到进阶学习源码中的巧妙之处,旨在于将学习的东西应用到实际的开发中,同时借鉴源码中的思想,规范自我开发,以及锻炼自己的开发思维。也欢迎大家加入大佬的源码共读活动。一起卷起来。

正文

我们经常会在本地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命令中 选择调试命令,会进入调试模式。

image.png

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

image.png 那源码部分我们看完了 接下来我们认识一下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.形成了 断点调试 +源码主线+ 知识点补充 + 总结的源码学习方式