源码学习—— callback如何promisify化

103 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第4天,点击查看活动详情

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

这是源码共读的第2期,链接:juejin.cn/post/708315…

前言

如何将一个callback转换为promise,在学习remote-git-tags这个库的时候,看到其引用了node原生模块的promisify,因此一探究竟。

remote-git-tags库源码解析

该库的作用是:获取远程仓库的所有tag信息。

其原理就是:git ls-remote --tags repoUrl(远程仓库地址)

看下其源码实现:

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

代码包含注释总共22行,很精简的代码,下面分析下其主要流程:

  1. 首先通过git ls-remote --tasg命令,获取所有的tag。
  2. 其次将获取到的tag字符串,以\n分割为数组,并遍历,提取每一个tag的名字和hash值。
  3. 最后,以tagName为键,hash为值存入到生产的map中,并返回该map。

node的模块机制

在源码中,我们看到其引入的模块方式是ES6的模块机制,import {promisify} from 'node:util';

在Node 13之前,Node都是CommonJS的模块机制,在Node 13中添加了对标准ES6模块的支持。因此,我们在本库中,看到其引入node模块都是通过import from.

如何告知node加载什么模块:

一种方式,就是改变文件的扩展名。如果是.mjs结尾的文件,则Node就会将其作为ES6模块来加载。如果是.cjs结尾的文件,则Node将其作为CommonJS模块加载。

另一种方式就是在package.json文件中,设置type字段。属性值为module则使用ES6模块。值为commonjs或者为空的话就默认以commonjs模块加载。

node的promisify方法

其原理就是将异步方法中最后一个参数是回调函数,且回调函数中有两个参数:errordata,将该callback promise化。 看下其使用:

// 使用前
fs.readFile('xx/xxx.json', (err, data) => {})
// 使用promisify后
const readFilePromise = promisify(fs.readFile)
readFilePromise('./index.js')
  .then(data => {})
  .catch(err => {})

实现一个promisify方法

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

  • 返回一个函数,该函数会将传入来的异步方法用promise来包裹。
  • args也就是传入的异步函数,其他的非回调函数的参数。
  • Reflect.apply()。在内部将回调函数合入传入的参数数组内,利用apply来执行,将参数传入其中。Reflect.apply就相当Function.prototype.apply.call(func, thisArg, args)

node模块的promisify方法

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;

相对与上面我们自己实现的方法,node的方法更健壮,考虑了更多的情况,加了对应的校验逻辑,但其核心原理是跟上面一致的。

收获

  • node的两种模块机制,这块在之前不太了解,本次源码学习扩展了node这块的知识面。
  • promisiy的实现原理,将具备一定通用规则的异步方法的callback prmoise化
  • Reflect,学习了ES6中为操作对象提供的新的API

思考

这里实现的promisify方法只能使最后一个参数是回调函数,且具备两个参数errordata promise化。只有这种规则的异步方法才可以用此方法。但我们了解其原理后,就可以利用这种方式将我们项目中的具备一定规则的异步方法promise化。