【若川视野 x 源码共读】第14期 | 从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现

144 阅读2分钟

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

学习目标

    1. Node 加载采用什么模块
    1. 获取 git 仓库所有 tags 的原理
    1. 学会调试看源码
    1. 学会面试高频考点 promisify 的原理和实现

环境准备

  • Node
  • Git 关于环境安装问题,网上已经有大量的文章,这里不再做阐述。
git clone https://github.com/lxchuan12/remote-git-tags-analysis.git

克隆相关的代码,找到remote-git-tags文件目录,执行:

yarn 或 npm install

测试代码

我们可以根据已有的代码找到 test.js 文件,可以对remoteGitTags方法进行测试。

remoteGitTags 源码

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;
}

promisify函数是把 callback 形式转成 promise 形式.

promisify 函数

准备一个异步请求图片的方法

const imageSrc = 'https://www.themealdb.com/images/ingredients/Lime.png';

const 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('Loading Error!'));
    document.body.append(image);
}

编写一个基础版本的CallBack调用方式


const cb = (err,imageData) =>{
            if(err){
                console.error(err);
                return;
            }
            console.log(imageData)
        }
loadImage(imageSrc, cb);

这个基础的CallBack调用方式可以实现图片的加载,但是使用CallBack的方式难免会因为回调函数过多造成回调地狱

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);
});

弊端很明显,我们不能决定调用哪一个函数,所以出现了优化版本。

Promise 进一步优化

function promisify(originalFun){
    function fn(...args){
        return new Promise((resolve, reject) => {
            args.push((err, ...values) => {
                if(err){
                    return reject(err);
                }
                resolve(values);
            });
            console.log(args)
            originalFun.apply(this, args);
            // Reflect.apply(originalFun, this, args);
        });
    }
    return fn;
}

const loadImagePromise = promisify(loadImage);
async function load(){
    try{
        const res = await loadImagePromise(imageSrc);
        console.log(res);
    }
    catch(err){
        console.log(err);
    }
}
load();

Node utils promisify 源码

utils 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;

总结

可能有些地方不是很清楚,但是大概知道promisify的实现原理,从学习中也看到了自己基础知识的不足。自己有空的时候会恶补基础知识。