Node.js 发布之初,对于异步操作,都是以 error first callback 风格提供执行结果。
fs.readFile('/foo.txt', (err, data) => {
// err非空时,表示出现异常
// data是操作的结果,这里是读取文件的内容
console.log(data);
});
对于简单的任务还好,如果一项任务涉及多个异步操作,容易出现代码多层嵌套;而并行的异步操作协同起来也不方便。
于是,Promise
被引入了。为了方便,Node官方的fs
包,甚至提供了promise
版本的各项函数:
fs.promises
.readFile("t.js")
.then((text) => {})
.catch((err) => {});
对于其它的已经存在的 error first callback 函数,Node 也提供了util.promisify
函数,以帮助转换:
const util = require("util");
util.promisify(fs.readFile);
以上是 callback风格 和 Promise风格 的一点简单历史。再回到主题上来:
如何让一个函数同时支持 Callback 和 Promise 风格?
先说说我们要的效果,它是这样的:
// 一个异步函数
function someFun(callback) {}
// 可以传callback执行
someFun(console.log);
// 可以返回Promise
someFun().then(console.log);
假设这个异步函数是在2秒后生成一个随机数。对于 callback 风格,可以这样写:
function someFun(callback) {
setTimeout(() => {
callback(null,Math.random());
}, 2000);
}
对于 Promise 风格,可以这样写:
function someFun() {
return new Promise((res) => {
setTimeout(() => {
res(Math.random());
}, 2000);
});
}
怎么将2种风格合并呢?
最初的思考是,判断 callback
参数是否存在,不存在则返回 Promise
,否则调用 callback
:
function someFun(callback) {
if (callback === undefined) {
return new Promise((res) => {
setTimeout(() => {
res(Math.random());
}, 2000);
});
}
setTimeout(() => {
callback(null, Math.random());
}, 200);
}
虽然运行起来没问题,但让人感觉别扭,因为 延时生成随机数 这块代码,重复了2次。
再一想,可以优化如下:
function someFun(callback) {
if (callback === undefined) {
return new Promise((res, rej) => {
someFun((err, data) => (err ? rej(err) : res(data)));
});
}
setTimeout(() => {
callback(null, Math.random());
}, 200);
}
让 Promise 复用 callback 的逻辑,达到了消除重复代码的问题。
不考虑具体功能,在编写自己的同时支持两种调用风格的代码时,得用以上模板会有问题吗?
请思考一会再往后读。
如果单纯按 延时生成随机数 这个要求,上述代码无可厚非。但我们写文章通常是为了要泛化(generalize)某个思考方式,使其具有普适性(更广的用途),而不是局限于某个特定的单一问题。
上述代码有问题吗?如果我们的异步函数内部使用了 this
,代码便会出现问题。
提供一个检验的方式供参考:将异步函数someFun
挂载到某个对象obj
下,然后以 promise 风格执行:
// 一个异步函数
function someFun(callback) {
console.log(this);
if (callback === undefined) {
return new Promise((res, rej) => {
someFun((err, data) => (err ? rej(err) : res(data)));
});
}
setTimeout(() => {
callback(null, Math.random());
}, 200);
}
const obj = {
someFun,
};
// 以 Promise 风格执行
obj.someFun().then(console.log);
你猜会打印什么?
控制台运行结果如下:
惊讶吗?
首先,打印了2次,因为callback
参数为空时,我们会以callback
方式重新执行someFun
函数。
更重要的问题是——2次打印的this
竟然不同!
对于同时支持两种风格的函数,如果其依赖了this
(比如使用了this
上的属性或方法),某些情况下你会得到意想不到的结果!
怎么解决?还记得 JavaScript 中每个函数都有call
和apply
方法吗?在我看来,它们都是函数的瑰宝(另外还有bind
),在解决this
问题上,总能帮上忙。
重整后的实现如下:
function someFun(callback) {
console.log(this);
if (callback === undefined) {
return new Promise((res, rej) => {
someFun.call(this, (err, data) => (err ? rej(err) : res(data)));
});
}
setTimeout(() => {
callback(null, Math.random());
}, 200);
}
注意 someFun.call(this,...)
的使用,它将 this
绑定为函数初始执行时的值(本例中即obj
)。再运行上面的例子,我们得到:
可以看到,2次打印的this
是一致的,即使你要this
上其它的属性和方法,也不会有问题了。
本文灵感来自于 github.com/nodejs/undici[1]。nodejs/undici 是 Node.js 官方维护的一个 http client 库,没用过的,推荐大家尝尝鲜。
github.com/nodejs/undici: https://github.com/nodejs/undici/blob/master/lib/client-request.js