昨天周会同事做技术分享,讲到一点:“用 Promise.then 容易出现回调地狱,所以改用 async/await”。那么这个说法对吗?
首先这个说法完全错误。“Promise.then 出现回调地狱”,完全是编码不规范的问题,而不是 api 的问题。来看下面这个例子:
// 解析压缩包
function parseZip(zip) {
return new Promise(
resolve => setTimeout(resolve, 1000, zip)
);
}
// 从压缩包中获取 yaml 文件
function getYaml(unpack) {
return new Promise(
resolve => setTimeout(resolve, 1000, unpack)
);
}
// 解析 yaml 文件为数组
function parseYaml(yaml) {
return new Promise(
resolve => setTimeout(resolve, 1000, yaml)
);
}
这边我们需要按顺序先后调用三个函数,即将第一个函数返回的结果传入第二个函数,将第二个函数返回的结果传入第三个函数,以此类推。如果编码不规范,就会出现下面这个情况:
parseZip("zip files")
.then(res => {
getYaml(res)
.then(res => {
parseYaml(res)
.then(res => {
console.log("最终结果:", res);
})
})
})
这样写问题很大,一方面出现回调地狱,另一方面异常捕获变得困难
那么按规范的写法是怎么样的呢?我们知道 then 方法是有返回值的,并且返回的是一个 Promsie。如果 then 方法内部的回调函数返回非 Promise 类型,那么 then 方法就会把这个值包裹在 Promise 中返回;如果 then 方法内部的回调函数本身返回一个 Promise,那么 then 方法直接把这个 Promise 透传出去。我们可以充分利用这一点来优化逻辑:
parseZip("zip files")
.then(res => getYaml(res))
.then(res => parseYaml(res))
.then(res => console.log(res))
.catch(err => logReport(err));
可以看到改为链式调用就变得优雅了,异常捕获也很简单,其中任意一步执行出错,都会中断整个 Promise 链,然后被最后的 catch 方法捕获到
其实还可以再进一步优化,我们知道 then 方法的入参就是一个回调函数,上面的代码中我们在外面包裹了一层函数用来透传参数,但实际上直接传递函数表达式即可:
parseZip("zip files")
.then(getYaml)
.then(parseYaml)
.then(console.log.bind(console))
.catch(logReport);
其次,这两句话前后根本没有因果关系。Promise.then 链式调用和 async/await 同步的写法,完全是两种编程风格而已。用 async/await 写着很舒服,但是异常捕获会变得比较麻烦:
async function execute() {
try {
const unpack = await parseZip("zip files");
const yaml = await getYaml(unpack);
const res = await parseYaml(yaml);
console.log(res);
} catch (error) {
console.log(error);
}
}
或者改成这样:
async function execute() {
const unpack = await parseZip("zip files");
const yaml = await getYaml(unpack);
const res = await parseYaml(yaml);
return res;
}
execute()
.then(res => console.log(res))
.catch(err => console.log(err));
可以看到其实并没有简单多少,本人在平时开发中主要还是在用 Promise.then,这样更方便一些:
this.loading = true;
parseZip("zip files")
.then(getYaml)
.then(parseYaml)
.then(console.log.bind(console))
.catch(err => {
// 捕获整个 Promise 链的异常
console.log(err);
})
.finally(() => {
// 无论成功或失败都要进行的操作
this.loading = false;
})