问题
最近组里的测试去测别的项目组的产品了,所以几个前端页面组里开发人员互相测,在查看一个页签切换的代码时发现了一个以前没注意到的现象:页签使用的elementui的tabs控件,使用before-leave钩子在切换页签前检查当前页签是否有没有填写的表单项
然后代码是这样的:
切换到graph页签时需要先检查basic页签是否有没填写的表单项,如果有则不允许切换,测试结果是符合要求的,但代码上看却有疑惑的地方,beforeLeave()方法应该返回一个Promise对象,tabs页签获取这个promise对象后应该是执行类似下面这种逻辑:
promise.then(()=>{
切换页签
}).catch(()=>{
不切换页签
})
但是await前并没有加return,async函数是怎么知道返回什么状态的promise?
解析
记得去年看《你不知道的javascript》时,上中下三卷,中卷几乎正本都在讲Promise、迭代器、生成器这块的内容(外国人写书非常详细),现在都忘得差不多了,重新百度了一下,结合《ECMAScript 6 入门》这本书和一些测试,差不多高明白了。
在js代码中,如果使用了promise,且promise最终状态是reject,可以通过promise.catch(fn)捕获,如果没有捕获,f12就会看到类似uncaught xxx之类的错误提示,但和普通异常不同,promise中抛出异常并不会中断代码执行,因为promise是异步的。
async函数总会返回Promise对象,这个是都知道的,比如函数中return true,async会处理成类似return Promise.resovle(true),然后通过then(fn(data))获取的data就是true。
我们知道async和await其实是使用生成器实现的,可以看成基于生成器的语法糖,下面是《ECMAscript6入门》中提供的一个关于语法糖的实现:
async function fn(args) {
// ...
}
// 等同于
function fn(args) {
return spawn(function* () {
// ...
});
}
function spawn(genF) {
return new Promise(function(resolve, reject) {
const gen = genF();
function step(nextF) {
let next;
try {
next = nextF();
} catch(e) {
return reject(e);
}
if(next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(function(v) {
step(function() { return gen.next(v); });
}, function(e) {
step(function() { return gen.throw(e); });
});
}
step(function() { return gen.next(undefined); });
});
}
按上述代码分析,step函数第一次调用时,表单校验不通过,await的promise最终为reject状态:
.catch(() => {
this.msgError('请检查必填项!')
return Promise.reject()
})
所以next.value最终为reject状态的promise,所以执行了step(function() { return gen.throw(e); });然后再次执行step函数,调用gen.throw(e),而异步函数beforeLeave中没有try catch代码块,所以异常会向外层抛出(生成器的throw()方法的特性),也就会在step函数中的此处被捕获:
try {
next = nextF();
} catch(e) {
return reject(e);
}
也就是说spawn函数最终返回了一个reject状态的promise,tabs页签得知这是一个reject状态的promise,就不会进行切换动作;相反的,当表单校验通过时,await修饰的promise最终是fullfill状态的,此时async生成的语法糖没有捕获到异常,由于代码中也没有返回任何值,所以async最终返回了一个Promise.resolve(),也是一个fullfill状态的promise,但其实和await修饰的那个promise并不是一个,不过只要是fullfill状态的promise,tabs页签都会进行切换。
综上,简单的说就是,如果await修饰的promise最终是reject状态且没有被捕获,async会捕获并返回Promise.reject(error),如果await修饰的promise是fullfill状态则async会return Promise.resolve(),与测试用例期望的情况恰巧是一致的,实际上代码可以简写为:
再补充一下,上面说的仅适用于await修饰的promise,如果一个promise没有被await修饰(没有被生成器的yield处理),且最终状态为reject,没有被捕,并不会成为async函数的返回值,在f12中可以看到未捕获异常的错误信息,但不会影响他后面的代码执行。