Skip to content

Promisification  #18

@canvascat

Description

@canvascat

“Promisification” 是用于一个简单转换的一个长单词。它指将一个接受回调的函数转换为一个返回 promise 的函数。

在 node 中的 util 模块中含 promisify 方法,可以将如 fs 模块的 readFile 方法转化为一个返回 promise 的方法。

const { promisify } = require('util');
const { readFile } = require('fs');
const { join } = require('path');

const filePath = join(__dirname, './test.txt');

readFile(filePath, (err, data) => {
  if (err) {
    // ...
  } else {
    // ...
  }
});

promisify(readFile)(filePath).then(
  (data) => {
    // ...
  },
  (err) => {
    // ...
  }
);

下面是 promisify 的源码:

const kCustomPromisifiedSymbol = SymbolFor('nodejs.util.promisify.custom');
const kCustomPromisifyArgsSymbol = Symbol('customPromisifyArgs');

function promisify(original) {
  if (typeof original !== 'function')
    throw new ERR_INVALID_ARG_TYPE('original', 'Function', original);

  // 判断是否有缓存,有则直接返回
  if (original[kCustomPromisifiedSymbol]) {
    const fn = original[kCustomPromisifiedSymbol];
    if (typeof fn !== 'function') {
      throw new ERR_INVALID_ARG_TYPE('util.promisify.custom', 'Function', fn);
    }
    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) => {
      // 添加对 original 的自定义的回调到参数尾部
      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;

module.exports = {
  promisify,

  // Symbol used to customize promisify conversion
  customPromisifyArgs: kCustomPromisifyArgsSymbol,
};

对以上方法进行简化,可以基本满足使用:

function promisify(f, multiple = false) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      function callback(err, ...values) {
        if (err) {
          reject(err);
        } else {
          if (multiple && values.length > 1) {
            resolve(values);
          } else {
            resolve(values[0]);
          }
        }
      }

      args.push(callback);

      f.call(this, ...args);
    });
  };
}

这里根据第二个参数来决定回调的结果是第一个参数还是所有参数组成的数组。

如果需要对一些有奇特的回调格式的函数(如 callback(result))进行转换,就需要根据实际情况处理了。当然 github 还有类似 node 中的 util.promisify 的可以用于浏览器环境的模块,如 es6-promisify

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions