详解 Node.js 中 promisify 方法的源码

详解 Node.js 中 promisify 方法的源码

我正在参与掘金会员专属活动-源码共读第一期,点击参与

前言

本文要分析的源码是 Node.js 中的 promisify 方法,源码地址:github.com/nodejs/node…

使用方式

Node.js 是非阻塞式 I/O 的模型,简单来说,它就是异步的。在早期版本中,我们通常使用回调函数的方式来写 Node.js 的代码,回调函数的第一个参数是错误信息,错误优先。

我们来重现一下这种用法。

首先在项目的根文件目录下,新建 a.txtb.txt 文件,内容分别是 aabb,再新建 index.js,并书写以下 JavaScript 代码,分别读取这两个文件:

const fs = require("fs");
fs.readFile("./a.txt", "utf-8", (err, data) => {
    console.log(data); // aa
});
fs.readFile("./b.txt", "utf-8", (err, data) => {
    console.log(data); // bb
});
console.log('end');

VS Code 的终端输入 node index.js 命令,输出结果有两种:

详解 Node.js 中 promisify 方法的源码

详解 Node.js 中 promisify 方法的源码

readFile 是异步读取文件,所以不会阻塞下面代码的执行,于是就先输出了 end;由于是异步读取文件,相当于两个读取文件的操作同时进行,谁先读完,谁输出,所以 aabb 的输出顺序是不确定的。

如果要保持顺序来读取文件,也就是同步读取文件,有两种方法:

  1. ./a.txt 的回调函数中执行 ./b.txt 的读取操作,在 ./b.txt 的回调函数中输出 end,具体代码如下:
const fs = require("fs");
const util = require("util");
fs.readFile("./a.txt", "utf-8", (err, data1) => {
    console.log(data1);
    fs.readFile("./b.txt", "utf-8", (err, data2) => {
        console.log(data2);
        console.log("end");
    });
});
  1. 使用 readFileSync
const fs = require("fs");
const data1 = fs.readFileSync("./a.txt", "utf-8");
const data2 = fs.readFileSync("./b.txt", "utf-8");
console.log(data1); // aa
console.log(data2); // bb
console.log("end");

上述两种方式的输出结果都为:

详解 Node.js 中 promisify 方法的源码

很显然,第一种方式会造成恐怖的回调地狱问题,第二种方式解决同步问题的完美方案。

那有没有一种方法可以将开发者自定义的异步函数在不使用回调函数的方式的情况下,将其变为同步函数呢?有!那就是今天的主角 — promisify 方法。

promisify 方法只需要传入一个参数—错误优先的回调风格的异步函数(也就是将 (err, value) => ... 回调作为最后一个参数),并返回一个返回值是 Promise 对象的函数。

接下来,让我们使用 promisify 方法对 readFile 进行改造:

const fs = require("fs");
const util = require("util");
const myReadFileSync = util.promisify(fs.readFile);

async function loadFile() {
  const data1 = await myReadFileSync("./a.txt", "utf-8");
  const data2 = await myReadFileSync("./b.txt", "utf-8");
  console.log(data1);
  console.log(data2);
  console.log("end");
}
loadFile();

可见,功能上跟 readFileSync 基本一样。

promisify 方法虽然对 readFile 函数来说有点多此一举(因为已经有了 readFileSync),但是对我们开发者的异步编程来说,带来了极大的便利。

源码实现

Node.js 源码里的 promisify 方法用了很多其他模块中的方法,可能没那么通俗易懂,所以在这里实现一个简易版的 promisify,基本思路是和完整版的 promisify 一样的。

首先,我们将 promisify 的整个模板搭出来,再去细化到具体思路。

promisify 方法接收的是一个异步函数,返回的是一个返回值为 Promise 对象的函数,因此,我们可以很快地写出以下代码:

function myPromisify(originalFn) {
    function fn() {
        return new Promise((resolve, reject) => {
            // ....
        })
    }
    return fn;
}

接着再来回想一下,promisify 方法返回的函数在调用时,会传入除了异步函数的回调函数中的所有参数。比如 readFile 中的文件路径和编码参数,但 readFile 中的回调函数不会传入。

所以,异步函数中的回调函数肯定是在 Promise 内部中被补上了,为什么要这么做呢?因为需要通过回调函数中的 err 参数值来判断是否将 Promise 对象设置为 rejected 状态。具体逻辑是这样的:

  1. 如果 err 参数值不为空,那么就通过 reject 方法返回错误信息,Promise 对象设置为 rejected 状态。
  2. 否则,通过 resolve 方法返回成功操作时的数据,Promise 对象设置为 fulfilled 状态。

具体实现代码如下:

function myPromisify(originalFn) {
    function fn(...args) {
        return new Promise((resolve, reject) => {
            // 补上原异步函数中的回调函数
            args.push((err, data) => {
                if (err) {
                    return reject(err);
                }
                resolve(data);
            })
            // 调用原异步函数,等同于 originalFn.apply(this, args)
            Reflect.apply(originalFn, this, args);
        })
    }
    return fn;
}

最后不要忘记调用原异步函数。至此,一个简易版的 promisify 就完成了。

有关于 Reflect.apply 方法的讲解,可前往阮一峰老师的《ES6 标准入门教程》。

总结

promisify 方法的作用是赋予异步函数同步的能力,内部主要是通过 Promise 对象来实现,
reject 方法返回异步操作失败时的结果,resolve 方法返回异步操作成功时的结果。

原文链接:https://juejin.cn/post/7227745800460992573 作者:焦糖不丁

(0)
上一篇 2023年5月1日 上午10:20
下一篇 2023年5月1日 上午10:31

相关推荐

发表回复

登录后才能评论