【第14期】Promisify 源码分析

166 阅读3分钟

本次源码阅读分析的是第14期的内容, 主要通过 remote-git-tags 来学习 node 中的 promisify

首先简单介绍下 remote-git-tags, 这个库的作用是:从远程仓库获取所有标签。

通过执行 git ls-remote --tags repoUrl (仓库路径)获取 tags

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

源码行数并不是很多, 逻辑也很清晰, 下面梳理下自己第一次遇到的知识

  1. 引入 node 模块 nodejs.org/dist/latest…

    import {promisify} from 'node:util';
    import childProcess from 'node:child_process'
    
  2. package.json 文件中 "type": "module" 的作用

  3. promisify 采用遵循常见的错误优先的回调风格的函数(也就是将 (err, value) => ... 回调作为最后一个参数),并返回一个返回 promise 的版本。

下面着重分析一下 promisify

//1. SymbolFor / ObjectDefineProperty 等函数, 实际上就是将 js 的内置函数进行保存, 以防止用户修改了内置函数. 
const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom'); // 使用symbol.for 保证获取的是同一个 symbol 值
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');
​
//2. validateFunction 验证 original 是不是函数
let validateFunction;
​
function promisify(original) {
  // Lazy-load to avoid a circular dependency.
  if (validateFunction === undefined)
    ({ validateFunction } = require('internal/validators'));
​
  validateFunction(original, 'original');
​
  //3. 这里的判断是否有自定义的 promise 化函数, 如果有直接返回 参见 http://nodejs.cn/api/util.html#custom-promisified-functions
  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.
  //4. 如果回调函数有多个值, 则可以通过自定义参数来获取一个保存了这些值的对象.
  const argumentNames = original[kCustomPromisifyArgsSymbol];
​
  // 5. 关键函数 fn, 返回了一个 promise 对象, 提供了一个通用的用于处理 *错误优先的回调风格的函数* 的回调函数(被!!!包裹的部分)
  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]);
        }
      });
      // 执行 original 函数, 绑定 this, 输入 args
      ReflectApply(original, this, args);
    }!!!);
  }
​
  // 将 fn 的原型设置为 original 的原型
  ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
  // fn 上挂载一个 symbol 属性,指向 fn
  ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
    value: fn, enumerable: false, writable: false, configurable: true
  });
  // 将 original 上其它属性复制到 fn 上, 并返回
  return ObjectDefineProperties(
    fn,
    ObjectGetOwnPropertyDescriptors(original)
  );
}
​
// 6. 将 kCustomPromisifiedSymbol 挂载到 promisify 的 custom 属性上
promisify.custom = kCustomPromisifiedSymbol;
  1. SymbolFor ObjectDefineProperty 等函数

    这些函数是在 \lib\internal\per_context\primordials.js 中定义的, 实际上就是将 js 的内置函数进行保存, 以防止用户修改了内置函数.

  2. validateFunction 验证 original 是不是函数

    const validateFunction = hideStackFrames((value, name) => {
      if (typeof value !== 'function')
        throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
    });
    
  3. 这里的判断是否有自定义的 promise 化函数, 如果有直接返回, 替代了下面的 fn函数, 这样有利于非错误优先的回调作为最后一个参数的标准格式, 详细可以参见 nodejs.cn/api/util.ht…

  4. 如果回调函数有多个值, 则可以通过自定义参数来获取一个保存了这些值的对象.

    {
      const firstValue = 5;
      const secondValue = 17;
    ​
      function fn(callback) {
        callback(null, firstValue, secondValue);
      }
    ​
      fn[customPromisifyArgs] = ['first', 'second'];
    ​
      promisify(fn)().then(common.mustCall((obj) => {
        assert.deepStrictEqual(obj, { first: firstValue, second: secondValue });
      }));
    }
    
  5. nodejs.cn/api/util/ut…

  6. 将 kCustomPromisifiedSymbol 挂载到 promisify 的 custom 属性上, 这样用户即可自定义 promisify 函数

    doSomething[util.promisify.custom] = (foo) => {
      return getPromiseSomehow();
    };
    

上述就是对 promisify 的源码解读, 阅读后还有一些地方不甚了解, 希望得到大家的帮助解答.

问题:

  1. 这些代码我理解是将 original 的东西都复制给 fn, 这样做是为了解决什么问题呢?
...  
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));
...
return ObjectDefineProperties(
    fn,
    ObjectGetOwnPropertyDescriptors(original)
  );
  1. 在 fn 上还设置这样的一个属性有什么用呢?

      ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
        value: fn, enumerable: false, writable: false, configurable: true
      });
    

TODO:

  1. 梳理模块化知识
  2. 复习 promise
  3. ts namespace