场景
我们知道异步最直接的处理方式 就是:回调函数。比如下面这个读取文件的模拟函数:
js
const readFile = (path, callback) => {
setTimeout(() => {
callback(null, 'Hello World');
}, 1000);
}
我们尝试使用这个函数去读取文件:
js
readFile('xx/hello.txt', (err, data) => {
if (err) {
console.error(err);
return;
}
console.log(data);
})
简单测试一下发现1s之后打印 Hello World,这没问题。
需求
但是大多数时候我们可能更希望是这种代码结构:
js
const data = await readFile('xx/hello.txt')
console.log(data)
当然,为了错误处理,我们可能还会再包一层 try catch
。
简单处理
为了实现这个需求,我们可能想到的最简单的实现是这样的:
js
const work = async () => {
const data = await new Promise((resolve, reject) => {
readFile((err, data) => {
if (err) {
reject(err);
}
resolve(data)
})
})
console.log(data)
}
work()
就是简单用 Promise 去包装一下。
封装
当然我们肯定不希望这样手动去使用promise包裹,如果手动写这个promise,代码写起来显得画蛇添足。我们希望的是要是有一个工具,可以自动帮我们把回调的形式自动包装成promise就好了。比如:
js
const readFileFn = promisify(readFile)
接下来我们再这样区去使用:
js
const work = async () => {
const data = await readFileFn('xx/hello.txt');
console.log(data)
}
work()
我们先简单分析一下,可以得到以下几点值得注意的地方:
- readFileFn 调用的时机就是 readFile 调用的时机。
- promisify 调用的结果是返回一个 promise,这个 promise 完成的时机是 callback 调用的时机。
所以我们的思路是包装一个函数传给 readFile,这个函数接收的最后一个参数就是 callback,而这个函数的返回值是一个 promise。由于函数是我们包装的,所以最后一个参数 callback 的实现由我们控制,那么我们可以在 callback 的实现中,修改先前生成的 promise 的状态。具体完整代码如下:
js
const promisify = (fn) => (...args) => new Promise((resolve, reject) => {
args.push((err, data) => {
err ? reject(err) : resolve(data);
})
fn(...args);
})
最后再完整测试一下:
js
const readFile = (path, callback) => {
setTimeout(() => {
callback(null, 'Hello World');
}, 1000);
}
const promisify = (fn) => (...args) => new Promise((resolve, reject) => {
args.push((err, data) => {
err ? reject(err) : resolve(data);
})
fn(...args);
})
const readFileFn = promisify(readFile)
const work = async () => {
const data = await readFileFn('xx/hello.txt');
console.log(data)
}
work()
发现测试是OK的。同时这也是 util 这个工具包,promisify方法的源码实现逻辑。不同的是,在源码中,这个方法更多地考虑了函数属性的处理问题,不过我们这里已经实现了其核心逻辑。 值得一提的是:通过这个方法将 callback 包装成 promise 形式,需要 callback 设计放在最后一个参数位置,就像我们例子中的 readFile 方法。