实现Promisify

将传统的回调函数风格的异步函数转换为返回Promise的函数

问题

在Node.js和传统的JavaScript异步编程中,很多API使用回调函数(callback)的方式处理异步操作,通常遵循”错误优先”的回调约定(error-first callback),即回调函数的第一个参数是错误对象,后续参数是结果数据。

Promisify的作用是将这种回调风格的函数转换为返回Promise的函数,使代码更加现代化,支持async/await语法,提高代码可读性。

解答

/**
 * 将回调风格的函数转换为返回Promise的函数
 * @param {Function} fn - 需要转换的函数(遵循error-first callback约定)
 * @returns {Function} 返回Promise的函数
 */
function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      // 在原参数后添加回调函数
      fn.call(this, ...args, (err, ...results) => {
        if (err) {
          // 如果有错误,reject Promise
          reject(err);
        } else {
          // 如果成功,resolve结果
          // 如果只有一个结果,直接返回;多个结果返回数组
          resolve(results.length <= 1 ? results[0] : results);
        }
      });
    });
  };
}

/**
 * 进阶版:支持自定义回调参数位置
 * @param {Function} fn - 需要转换的函数
 * @param {Object} options - 配置选项
 * @returns {Function} 返回Promise的函数
 */
function promisifyAdvanced(fn, options = {}) {
  const { multiArgs = false } = options;
  
  return function (...args) {
    return new Promise((resolve, reject) => {
      const callback = (err, ...results) => {
        if (err) {
          reject(err);
        } else {
          // multiArgs为true时,始终返回数组
          resolve(multiArgs ? results : results[0]);
        }
      };
      
      // 保持this上下文
      fn.call(this, ...args, callback);
    });
  };
}

使用示例

// 示例1:基础使用 - 转换fs.readFile
const fs = require('fs');

// 原始回调风格
fs.readFile('test.txt', 'utf8', (err, data) => {
  if (err) {
    console.error(err);
    return;
  }
  console.log(data);
});

// 使用promisify转换
const readFilePromise = promisify(fs.readFile);

// 使用Promise方式
readFilePromise('test.txt', 'utf8')
  .then(data => console.log(data))
  .catch(err => console.error(err));

// 使用async/await方式
async function readFile() {
  try {
    const data = await readFilePromise('test.txt', 'utf8');
    console.log(data);
  } catch (err) {
    console.error(err);
  }
}

// 示例2:自定义回调函数
function getUserInfo(userId, callback) {
  setTimeout(() => {
    if (userId <= 0) {
      callback(new Error('Invalid user id'));
    } else {
      callback(null, { id: userId, name: 'John', age: 25 });
    }
  }, 1000);
}

const getUserInfoPromise = promisify(getUserInfo);

getUserInfoPromise(1)
  .then(user => console.log(user)) // { id: 1, name: 'John', age: 25 }
  .catch(err => console.error(err));

// 示例3:处理多个返回值
function getMultipleValues(callback) {
  setTimeout(() => {
    callback(null, 'value1', 'value2', 'value3');
  }, 1000);
}

const getMultipleValuesPromise = promisifyAdvanced(getMultipleValues, { 
  multiArgs: true 
});

getMultipleValuesPromise()
  .then(results => console.log(results)); // ['value1', 'value2', 'value3']

// 示例4:保持this上下文
const obj = {
  name: 'MyObject',
  getData(callback) {
    setTimeout(() => {
      callback(null, `Data from ${this.name}`);
    }, 1000);
  }
};

obj.getDataPromise = promisify(obj.getData);

obj.getDataPromise()
  .then(data => console.log(data)); // 'Data from MyObject'

关键点

  • Error-First Callback约定:回调函数的第一个参数必须是错误对象,这是Node.js的标准约定

  • 参数传递:使用剩余参数(…args)收集原函数的所有参数,并在调用时展开传递

  • 回调函数注入:在原参数列表末尾添加自定义的回调函数,用于捕获异步结果

  • Promise封装:根据回调的第一个参数(err)判断成功或失败,分别调用resolve或reject

  • this上下文保持:使用fn.call(this, …)确保转换后的函数保持原有的this上下文

  • 多返回值处理:当回调函数有多个结果参数时,可以选择返回单个值或数组

  • 兼容性考虑:Node.js内置了util.promisify方法,实际项目中可以直接使用,但理解其实现原理对深入掌握Promise很有帮助