【若川视野 x 源码共读】第14期 | promisify

257 阅读3分钟

第14期 | promisify

【源码】promisify.png

学习目标:

(1)promisify可以实现什么功能?

(2)promisifyPromise有什么关系?

一、初识promisify

remote-git-tags用于读取指定git仓库的tags和对应的hash值。index.js代码不多,主要用到了node中两个方法,一个是util中的promisify,一个是child_processexecFile

1、引入promisifychildProcess
2、包裹childProcess.execFile方法
3、执行包裹后的execFile方法

const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);可以理解成在git命令行窗口输入这样一行命令: git ls-remote --tags https://github.com/sinndresorhus/got,得到的结果如图所示:

promisify_1.png

4、解析返回的数据
5、返回结果
import { promisify } from 'node:util';
import childProcess from 'node:child_process';

// 将 childProcess.execFile 用 promisify 包裹起来
const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
    // 等待 包裹了一层 promisify 的 execFile 执行成功,并返回结果
    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');
        const tagName = tagReference.replace(/^refs\/tags\//, ''),replace(/\^{}$/, '');
        tags.set(tagName, hash);
    }

    return tags;
}

二、execFile方法

如果想要从remote-git-tagsindex.js中了解promisify可以实现的功能,首先需要了解execFile是如何使用的。

通过查阅API,结合上面的例子得知,execFile用于执行特定的程序,将参数以数组的形式传入,然后用回调函数将结果返回。举例如下:

import childProcess from 'node:child_process';

childProcess.execFile('git', ['ls-remote', '--tags', 'https://github.com/sinndresorhus/got'], (error, stdout, stderr) => {
    console.log(stdout);
})
import childProcess from 'node:child_process';

childProcess.execFile('node', ['--version'], (error, stdout, stderr) => {
    console.log(stdout);
})

接下来回到remote-git-tags中查看包裹了一层的execFile方法是如何使用的:

const { stdout } = await execFile('git', ['ls-remote', '--tags', repoUrl]);

是不是有种似曾相识的感觉?没错,这里用到了ES6中的await语法。用于等待某个异步方法返回结果之后,再继续执行后面的操作。大胆猜想一下,promisify方法是不是将原本需要在回调函数中获取的内容,改成了通过Promise返回。

三、promisify源码

Node util 源码

以下是util 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, {
      __proto__: null,
      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, {
    __proto__: null,
    value: fn, enumerable: false, writable: false, configurable: true
  });

  const descriptors = ObjectGetOwnPropertyDescriptors(original);
  const propertiesValues = ObjectValues(descriptors);
  for (let i = 0; i < propertiesValues.length; i++) {
    // We want to use null-prototype objects to not rely on globally mutable
    // %Object.prototype%.
    ObjectSetPrototypeOf(propertiesValues[i], null);
  }
  return ObjectDefineProperties(fn, descriptors);
}

promisify.custom = kCustomPromisifiedSymbol;
1、引入自带的校验方法

校验传入的参数是否是一个function

const validateFunction = hideStackFrames(value, name) => {
    if (typeof value !== 'function') {
        throw new ERR_INVALID_ARG_TYPE(name, 'Function', value);
    }
}
2、判断original[kCustomPromisifiedSymbol]是否存在

如果存在,则校验original[kCustomPromisifiedSymbol]是否是一个function,然后在对象上定义一个新属性,并返回。

3、定义一个方法fn
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);
    });
}
4、设置一个指定对象的原型到另一个对象
ObjectSetPrototypeOf(fn, ObjectGetPrototypeOf(original));

然后在对象上定义一个新属性:

ObjectDefineProperty(fn, kCustomPromisifiedSymbol, {
    __proto__: null,
    value: fn, enumerable: false, writable: false, configurable: true
});
5、获取对象所有自身属性的描述符,将值放入fn,并返回
const descriptors = ObjectGetOwnPropertyDescriptors(original);
const propertiesValues = ObjectValues(descriptors);
for (let i = 0; i < propertiesValues.length; i++) {
    // We want to use null-prototype objects to not rely on globally mutable
    // %Object.prototype%.
    ObjectSetPrototypeOf(propertiesValues[i], null);
}
return ObjectDefineProperties(fn, descriptors);

四、将回调函数改造成Promise

1、loadData.js
export function loadData([param1, param2, param3], cb) {
	console.log('222')
    setTimeout(() => {
		console.log('333')
		let a = [
			'loadData',
			param1,
			param2,
			param3
		]
        cb(a)
    }, 2000)
}
2、promisify.js
export function promisify(oldFunc) {
    function fn(...args) {
        return new Promise((resolve, reject) => {
            args.push((...values) => {
                console.log('444')
                resolve(values)
            })
			console.log('111')
            Reflect.apply(oldFunc, this, args);
        })
    }
    return fn
}
3、test.js
import { loadData } from './demo/loadData.js'
import { promisify } from './demo/promisify.js'

const loadDataPromise = promisify(loadData)
async function testLoad() {
    const content = await loadDataPromise(['111', '222', '333'])
    console.log('testLoad', content)
}

testLoad();

promisify_2.gif

五、收获

1、语法

1.1 Symbol.for()

Symbol.for()

1.2 Object.setPrototypeOf()

设置一个指定的对象的原型(即,内部 [[Prototype]] 属性)到另一个对象或 null。

Object.setPrototypeOf()

1.3 Object.getOwnPropertyDescriptors()

用来获取一个对象的所有自身属性的描述符。

1.4 Object.defineProperty()

1.5 Reflect

Reflect

2、promisify

(1)promisify可以实现将回调函数改造成Promise返回。

(2)promisify中用到了Promise语法,以此实现对回调函数的改造。

参考

remote-git-tags

Node child_process

Node util

Node util promisify

ES6 await