【若川视野 x 源码共读】第14期 Node.js中promisify的源码实现 学习笔记

107 阅读4分钟

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是测试文件;

image.png

package.json文件中找到script脚本命令区域,如下图,我们需要启动项目,方法有多种(注意由于我默认保存会格式化代码,与xo自带eslint冲突,因此我把test中xo删除了,原项目的test命令与下图的dev一样),下面罗列了几种:

- 终端执行npm run test;
    或
- 鼠标放到test脚本命令上,会悬浮提示执行【运行脚本 | 调试脚本】,点击调试脚本;

image.png 启动项目后,就会停留在我们打的断点处,就可以进行调试。

image.png

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;
}
  1. 导入promisify: promisify是node中用于把异步的回调函数转换成promise的形式的方法,后面我们会讲讲promisify的实现。
import { promisify } from "node:util";
  1. 导入childProcess: childProcess是node中用于创建子进程的方法,详情可看childProcess。 并利用promisify对异步执行子进程命令的过程转成promise对象。
import childProcess from "node:child_process";
const execFile = promisify(childProcess.execFile);
  1. 封装获取标签的方法: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对象中。

  1. test.js文件中调用remoteGitTags('https://github.com/sindresorhus/got'),完成整个过程。

3.promisify的实现

promisify函数是把 异步callback 形式转成 promise 形式,我们知道 Node.js 天生异步,错误回调的形式书写代码。回调函数的第一个参数是错误信息。也就是错误优先。

  1. 简单的异步回调:
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函数,实现图片加载完成后或者失败时,进行回调。

  1. 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形式的被局限在转换图片加载了,我们需要封装一个通用化的函数,接收函数参数,该函数参数是各种异步回调函数。

  1. 通用的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;