【源码共读】| promisify

647 阅读2分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第6天,点击查看活动详情

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

【若川视野 x 源码共读】第14期 | promisify 点击了解本期详情一起参与

今天阅读的是:remote-git-tags

github.com/sindresorhu…

image-20221212143430771


尝试使用

import remoteGitTags from 'remote-git-tags';

console.log(await remoteGitTags('https://github.com/sindresorhus/remote-git-tags'));
//=> Map {'v1.0.0' => '69e308412e2a5cffa692951f0274091ef23e0e32', …}
  • 这个库的作用就是获取远程端的所有标签

源码分析

package.json

  • 我们可以看到入口文件index.js

image-20221212152239844

测试用例

  • 测试用例也非常简单
import test from 'ava';
import remoteGitTags from './index.js';

test('main', async t => {
    const tags = await remoteGitTags('https://github.com/sindresorhus/got');
    t.is(tags.get('v6.0.0'), 'e5c2d9e93137263c68db985b3dc5b57865c67b82');
    t.is(tags.get('v5.0.0'), '0933d0bb13f704bc9aabcc1eec7a8e33dc8aba51');
});

主文件

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

const execFile = promisify(childProcess.execFile);

export default async function remoteGitTags(repoUrl) {
	// 执行 git ls-remote --tags
	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');

		// 返回结果  举个例子:
		// d6602ec5194c87b0fc87103ca4d67251c76f233a	refs/tags/v0.99
		// f25a265a342aed6041ab0cc484224d9ca54b6f41	refs/tags/v0.99.1


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

代码非常简短,我们可以容易的知道怎么实现的。在代码中有一个值得注意的点,执行命令行的execFile中有一个promisify

那么,这个promisify的作用是啥,具体是怎么实现的呢

  • nodejs.org/dist/latest…

  • promisify是将需要传入回调函数的方法转化成promise的形式,解决了需要传入回调函数导致回调地狱的问题

promisify

我们直接看promisify可能有点难理解,我们从一个例子开始

读取当前文件夹的内容

我们可以写出以下代码:

// index.ts
import { readdir } from 'fs'

readdir('.', 'utf-8', (err, files) => {
  if (err) throw err
  console.log(files)
})

image-20221213085613505

我们将回调函数改为promise的形式

  • promise解决了回调地狱的问题
const readdirPromise = (fileSrc) => {
  return new Promise((resolve, reject) => {
    readdir(fileSrc, (err, files) => {
      if (err) {
        reject(err)
        return
      }
      resolve(files)
    })
  })
}

readdirPromise('.').then((res) => {
  console.log(res)
})

readdirPromise('.').catch((err) => {
  console.log(err)
})

上述写法能基本满足需求,我们对其进行进一步抽离,使得这个方法更具通用性

import { readdir } from 'fs'

const readFile = (filePath) =>
  readdir(filePath, 'utf-8', (err, files) => {
    if (err) throw err
    console.log(files)
  })

const promisify = (original) => {
  return function fn(...args) {
    return new Promise((resolve, reject) => {
      args.push((err, ...values) => {
        if (err) {
          return reject(err)
        }
        resolve(values)
      })
      // 赋值操作
      // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Reflect/set
      Reflect.apply(original, this, args)
    })
  }
}

const readFilePromise = promisify(readFile)
async function readFilePromisify() {
  try {
    const res = await readFilePromise('.')
    console.log(res)
  } catch (err) {
    console.log(err)
  }
}
readFilePromisify()

源码

接下来,我们来看看源码中是怎么实现的

module.exports = function promisify(orig) {
    // 校验orig参数合法性
    if (typeof orig !== 'function') {
        // 省略...
    }
	// 校验promisify.custom 合法性
    if (hasSymbols && orig[kCustomPromisifiedSymbol]) {
        // 省略...
    }

    // Names to create an object from in case the callback receives multiple
    // arguments, e.g. ['stdout', 'stderr'] for child_process.exec.
    var argumentNames = orig[kCustomPromisifyArgsSymbol]

    var promisified = function fn() {
        var args = $slice(arguments)
        var self = this // eslint-disable-line no-invalid-this
        return new Promise(function (resolve, reject) {
            orig.apply(
                self,
                $concat(args, function (err) {
                    var values = arguments.length > 1 ? $slice(arguments, 1) : []
                    if (err) {
                        reject(err)
                    } else if (
                        typeof argumentNames !== 'undefined' &&
                        values.length > 1
                    ) {
                        var obj = {}
                        $forEach(argumentNames, function (name, index) {
                            obj[name] = values[index]
                        })
                        resolve(obj)
                    } else {
                        resolve(values[0])
                    }
                })
            )
        })
    }

    promisified.__proto__ = orig.__proto__ // eslint-disable-line no-proto

    Object.defineProperty(promisified, kCustomPromisifiedSymbol, {
        configurable: true,
        enumerable: false,
        value: promisified,
        writable: false
    })
    var descriptors = getOwnPropertyDescriptors(orig)
    forEach(descriptors, function (k, v) {
        try {
            Object.defineProperty(promisified, k, v)
        } catch (e) {
            // handle nonconfigurable function properties
        }
    })
    return promisified
}

总结

  • 通过对remote-git-tags的分析,我们知道其执行了命令git ls-remote --tags来获取远程仓库的tag,通过正则截取了当前的版本号。
  • 通过对源码的分析,我们知道有一个promisify的函数实现原理。该方法传入需要回调函数作为参数的方法,转化为Promise的形式处理,在其内部通过Reflect.apply来传递参数。