JS 错误优先的回调函数到 `Promise` 的转换 (promisify)

各位观众,各位朋友,欢迎来到今天的“老司机带你飞:JS 错误优先回调函数到 Promise 的华丽转身”专题讲座!

今天咱们要聊的是一个在 JavaScript 开发中经常遇到的问题:如何把那些基于“错误优先回调”风格的函数,优雅地转换成基于 Promise 的函数。这就像把老式的拨号上网升级成光纤,速度和体验直接提升 N 个档次!

啥是“错误优先回调”?

先来回顾一下什么是“错误优先回调”风格的函数。 简单来说,就是指那些回调函数的第一个参数通常用于传递错误信息,如果操作成功,则为 nullundefined,而后续参数则用于传递实际的结果。

fs.readFile('./myfile.txt', 'utf8', (err, data) => {
  if (err) {
    console.error('读取文件出错:', err);
    return;
  }
  console.log('文件内容:', data);
});

上面的 fs.readFile 就是一个典型的例子。 err 参数用来判断有没有出错, data 参数才是真正读取到的文件内容。这种模式在 Node.js 中非常常见,属于历史遗留问题。

为啥要转成 Promise?

那么,为什么要费劲巴拉地把这些回调函数转换成 Promise 呢? 原因很简单:Promise 可以让我们更好地处理异步操作,避免回调地狱 (callback hell),让代码更易读、易维护。 Promise 提供了 thencatchfinally 等方法,以及 async/await 语法糖,让异步代码看起来更像同步代码,逻辑更清晰。

手动 Promisify:手搓的快乐

最直接的方法就是自己手动实现 promisify 函数。 让我们一起来看看如何手搓一个通用的 promisify 函数:

function promisify(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, ...result) => {
        if (err) {
          return reject(err);
        }
        if (result.length <= 1) {
          resolve(result[0]);
        } else {
          resolve(result);
        }
      });
    });
  };
}

这段代码的核心思想是:

  1. 接收一个“错误优先回调”风格的函数 fn 作为参数。
  2. 返回一个新的函数,这个新函数会返回一个 Promise 对象。
  3. 在新函数内部,调用原始函数 fn,并将 resolvereject 函数作为回调函数传递给 fn
  4. 在回调函数中,判断是否有错误发生,如果有错误,则调用 reject 函数;否则,调用 resolve 函数,并将结果传递给它。

代码解释:

  • fn.call(this, ...args, ...): 使用 call 方法是为了确保原始函数 fn 在正确的 this 上下文中执行。 ...args 用于传递原始函数的参数。
  • (err, ...result) => { ... }: 这是传递给原始函数的回调函数。 err 是错误对象, ...result 是剩余的结果参数。
  • if (result.length <= 1) { resolve(result[0]); } else { resolve(result); }: 这段代码处理了原始函数返回单个结果或多个结果的情况。 如果只有一个结果,则直接 resolve 这个结果;否则,将所有结果作为一个数组 resolve。

使用示例:

const fs = require('fs');
const promisify = (fn) => { /* 上面的 promisify 代码 */ };

const readFileAsync = promisify(fs.readFile);

readFileAsync('./myfile.txt', 'utf8')
  .then(data => {
    console.log('文件内容:', data);
  })
  .catch(err => {
    console.error('读取文件出错:', err);
  });

手动 Promisify 的进阶版本:考虑 this 上下文

上面的 promisify 函数已经可以工作了,但是它没有很好地处理 this 上下文。 如果原始函数依赖于 this 上下文,那么我们需要在 promisify 函数中正确地绑定 this

function promisify(fn) {
  return function (...args) {
    const self = this; // 保存 this 上下文
    return new Promise((resolve, reject) => {
      fn.call(self, ...args, (err, ...result) => {
        if (err) {
          return reject(err);
        }
        if (result.length <= 1) {
          resolve(result[0]);
        } else {
          resolve(result);
        }
      });
    });
  };
}

现成的 Promisify 工具:站在巨人的肩膀上

当然,我们不必每次都自己手写 promisify 函数。 Node.js 的 util 模块已经提供了一个 promisify 函数,可以直接使用。

const fs = require('fs');
const util = require('util');

const readFileAsync = util.promisify(fs.readFile);

readFileAsync('./myfile.txt', 'utf8')
  .then(data => {
    console.log('文件内容:', data);
  })
  .catch(err => {
    console.error('读取文件出错:', err);
  });

更方便了,有木有!

处理多个参数:化繁为简

有些“错误优先回调”风格的函数可能会返回多个结果,例如:

function myAsyncFunc(arg1, arg2, callback) {
  setTimeout(() => {
    if (arg1 < 0) {
      callback(new Error('arg1 must be positive'));
    } else {
      callback(null, arg1 * 2, arg2 + ' processed');
    }
  }, 100);
}

在 promisify 之后,我们需要确保能正确处理这些结果。 util.promisify 默认会将多个结果打包成一个数组。

const util = require('util');
const myAsyncFuncPromise = util.promisify(myAsyncFunc);

myAsyncFuncPromise(5, 'hello')
  .then(results => {
    console.log('结果:', results); // 输出: 结果: [ 10, 'hello processed' ]
  })
  .catch(err => {
    console.error('出错:', err);
  });

如果希望返回一个对象,可以自定义 promisify 函数:

function promisifyObject(fn) {
  return function (...args) {
    return new Promise((resolve, reject) => {
      fn.call(this, ...args, (err, result1, result2) => { // 假设有两个结果
        if (err) {
          return reject(err);
        }
        resolve({ result1, result2 }); // 返回一个对象
      });
    });
  };
}

const myAsyncFuncPromiseObject = promisifyObject(myAsyncFunc);

myAsyncFuncPromiseObject(5, 'hello')
  .then(results => {
    console.log('结果:', results); // 输出: 结果: { result1: 10, result2: 'hello processed' }
  })
  .catch(err => {
    console.error('出错:', err);
  });

promisify.custom:高级定制

Node.js 的 util.promisify 还提供了一个 promisify.custom 符号,允许我们自定义 promisify 的行为。 这对于一些特殊的函数或者需要进行额外处理的情况非常有用。

例如,我们可以为 fs.readFile 添加一个自定义的 promisify 版本,在读取文件之后自动将内容转换为大写:

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

fs.readFile[promisify.custom] = (path, options) => {
  return new Promise((resolve, reject) => {
    readFile(path, options, (err, data) => {
      if (err) {
        reject(err);
      } else {
        resolve(data.toUpperCase()); // 转换为大写
      }
    });
  });
};

const readFileAsync = promisify(fs.readFile);

readFileAsync('./myfile.txt', 'utf8')
  .then(data => {
    console.log('文件内容 (大写):', data);
  })
  .catch(err => {
    console.error('读取文件出错:', err);
  });

在这个例子中,我们通过 fs.readFile[promisify.custom] 定义了一个自定义的 promisify 函数。 当我们调用 promisify(fs.readFile) 时,实际上会调用我们自定义的函数。

错误处理:防患于未然

在使用 Promise 时,错误处理非常重要。 我们需要确保能够捕获所有可能发生的错误,避免程序崩溃。

  • try...catch 块: 可以在 async/await 函数中使用 try...catch 块来捕获错误。

    async function readFileAndProcess(filePath) {
      try {
        const data = await readFileAsync(filePath, 'utf8');
        const processedData = data.toUpperCase();
        console.log('处理后的数据:', processedData);
      } catch (err) {
        console.error('出错:', err);
      }
    }
    
    readFileAndProcess('./myfile.txt');
  • .catch() 方法: 可以在 Promise 链中使用 .catch() 方法来捕获错误。

    readFileAsync('./myfile.txt', 'utf8')
      .then(data => {
        const processedData = data.toUpperCase();
        console.log('处理后的数据:', processedData);
      })
      .catch(err => {
        console.error('出错:', err);
      });
  • 全局错误处理: 可以设置全局错误处理函数来捕获未处理的 Promise rejection。 在 Node.js 中,可以使用 process.on('unhandledRejection', ...) 来实现。

    process.on('unhandledRejection', (reason, promise) => {
      console.error('未处理的 rejection:', reason, promise);
    });

总结:选择适合你的方案

方案 优点 缺点 适用场景
手动 Promisify 灵活,可定制,理解 Promisify 的原理 代码量大,容易出错 需要高度定制,或者希望深入理解 Promisify 原理
util.promisify 简单易用,Node.js 内置,性能较好 功能有限,无法处理特殊情况 大部分情况,尤其是 Node.js 内置模块的回调函数
promisify.custom 灵活,可以自定义 Promisify 的行为 需要理解 Symbol 和 promisify.custom 的用法 需要对特定函数进行特殊处理,例如添加额外的逻辑或修改返回值
第三方库 功能丰富,可能提供更高级的特性 可能引入额外的依赖,增加项目体积 需要特定功能,例如自动 Promisify 整个模块

总而言之,选择哪种方案取决于你的具体需求和偏好。 util.promisify 通常是首选,因为它简单易用且性能良好。 如果需要更高级的定制,可以考虑 promisify.custom 或手动 Promisify。

最后的忠告:不要过度 Promisify

虽然 Promise 很好用,但并不是所有回调函数都需要 Promisify。 对于一些简单的、同步的回调函数,或者只在内部使用的回调函数,Promisify 可能会增加代码的复杂性。 要根据实际情况进行权衡,选择最合适的方案。

好了,今天的“老司机带你飞:JS 错误优先回调函数到 Promise 的华丽转身”专题讲座就到这里。 希望大家有所收获,在实际开发中能够灵活运用这些技巧,写出更优雅、更易维护的代码! 下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注