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

265 阅读3分钟

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

在实际开发中把某方法回调函数的形式promise化可以说是很常用的手段了,比如开发微信小程序使用的wx.request如果使用promise封装就更好用。今天要学习的是node中promisyfy,它就是把callback函数转成promise形式的好能手!

1.学习资料

参考资料:川哥的文章

从22行有趣的源码库中,我学到了 callback promisify 化的 Node.js 源码实现

2.分析源码

首先看一个蛮有用的库:remote-git-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)通过 ls-remote --tag 命令获取所有的tag, 并输出到标准输出中

(2)遍历标准输出的每一行,提取tag的名字和hash值

(3)以名字为键,hash为值存在map中,最后返回map

这里面的亮点是:

import {promisify} from 'node:util';
import childProcess from 'node:child_process';

const execFile = promisify(childProcess.execFile);

这里面有promisify方法,是把callback异步方法转成promise的形式;还有child_process中的execFile方法,可以执行命令。

本次详细学习promisify方法, 其实在之前的玩具vite中就这样引入过代码:

const readFile = require('util').promisify(fs.readFile)

等价形式为:

import {promisify} from 'node:util';
const readFile = promisify(fs.readFile)

思考一下promisify的实现思路:

1.return 一个promise
2.new Promise中包装了原来的异步函数,并且成功回调中resolve,失败回调reject,形如:
 new Promise ((resolve,reject) => {
	 异步函数({
   	 success: function() {resolve},
     failed: function() {reject}
   })
 })

在川哥的代码中给出一步一步实现一个通用的promisify的过程:

// 一张青柠
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';
    // 加载成功给callback传参数(null, 图片dom)
    image.onload = () => callback(null, image);
  	// 加载失败给callback传参('加载失败')
    image.onerror = () => callback(new Error('加载失败'));
    document.body.append(image);
}

在第一个html中展示的是回调的方法:

<body>
    <script src="./loadImage.js"></script>
    <script>
      	// 调用loadImage方法, 回调函数中判断有没有错误。。。
        loadImage(imageSrc, function (err, image) {
            if (err) {
                console.log(err);
                return;
            }
            console.log(image);
        });
    </script>
</body>

第二个html中展示了对loadImage方法promise化的过程:

<body>
    <script src="./loadImage.js"></script>
    <script>
      	// 接受一个参数:图片地址
        const loadImagePromise = function(src){
          	// 返回一个promise
            return new Promise(function(resolve, reject){
                loadImage(src, function (err, image) {
                    if(err){
                      	// 加载失败reject
                        reject(err);
                        return;
                    }
                    // 加载成功resolve
                    resolve(image);
                });
            });
        };
        loadImagePromise(imageSrc).then(res => {
            console.log(res);
        })
        .catch(err => {
            console.log(err);
        });
    </script>
</body>

当然后面的调用处也可以改成async await的形式:

async function load(){
  const res = await loadImagePromise(imageSrc)
  console.log(res)
} 
load()

第三个html展示了更加通用的promise化的方法:

<body>
    <script src="./loadImage.js"></script>
    <script>
        function promisify(original){
            function fn(...args){
                return new Promise((resolve, reject) => {
                    args.push((err, ...values) => {
                        if(err){
                            return reject(err);
                        }
                        resolve(values);
                    });
                    // original.apply(this, args);
                    Reflect.apply(original, 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();
    </script>
</body>

node中的实现

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

3.调试代码

打好断点:

启动调试:

小插曲:

报错:提示文件的引入方式不正确(模块化)
对策:卸载node, 安装最新稳定版

4.收获和总结

1.node util 中 promisify的原理

2.remote-git-tags实现原理

3.child_process中的execFile方法的使用

学习完本期源码,您可以思考如下问题:

1.package.json中type字段的含义?

2.package.json中engines字段的含义?

3.git中使用什么命令查看远程仓库的tags?

4.如何引用node原生库?

5.是否了解node中child_process的execFile方法?

6.简述remote-git-tags的原理?

参考资料:

【1】blog.csdn.net/weixin_4397…

【2】zhuanlan.zhihu.com/p/412183990

【3】nodejs.cn/api-v16/chi…