我正在参与掘金会员专属活动-源码共读第一期,点击参与。
前言
本文要分析的源码是 Node.js
中的 promisify
方法,源码地址:github.com/nodejs/node…
使用方式
Node.js
是非阻塞式 I/O 的模型,简单来说,它就是异步的。在早期版本中,我们通常使用回调函数的方式来写 Node.js
的代码,回调函数的第一个参数是错误信息,错误优先。
我们来重现一下这种用法。
首先在项目的根文件目录下,新建 a.txt
和 b.txt
文件,内容分别是 aa
和 bb
,再新建 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
命令,输出结果有两种:
readFile
是异步读取文件,所以不会阻塞下面代码的执行,于是就先输出了 end
;由于是异步读取文件,相当于两个读取文件的操作同时进行,谁先读完,谁输出,所以 aa
和 bb
的输出顺序是不确定的。
如果要保持顺序来读取文件,也就是同步读取文件,有两种方法:
- 在
./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");
});
});
- 使用
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");
上述两种方式的输出结果都为:
很显然,第一种方式会造成恐怖的回调地狱问题,第二种方式解决同步问题的完美方案。
那有没有一种方法可以将开发者自定义的异步函数在不使用回调函数的方式的情况下,将其变为同步函数呢?有!那就是今天的主角 — 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
状态。具体逻辑是这样的:
- 如果
err
参数值不为空,那么就通过reject
方法返回错误信息,Promise
对象设置为rejected
状态。 - 否则,通过
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 作者:焦糖不丁