一句话总结
时至今日,因.then造成的各种弊病痛点,前端构建项目应该全面拥抱await放弃.then的写法。
背景
曾经我也是遗老,坚持.then不动摇。我曾经坚信async await的语法糖会造成性能损失,一直拒绝使用。在callback回调地狱和.then的Promise地狱中挣扎过无数趟之后,现在我只感觉await真香。
三个历史阶段
前端控制异步代码,有三个历史阶段。
callback阶段
常规用法
const requestApi = (cb)=>{
// 模拟接口返回
setTimeout(function(){
const result = ‘异步接口返回的response’
cb(result);
},100)
}
const changeText = (text)=>{
document.querySelector(’#app’).innerHTML = text;
}
requestApi(changeText)
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
优点
性能优异。在异步调用极少的情况下,不需要引入Promise和async的包,所以性能较好。
兼容性好。最古老的JS底层也支持回调方式,无需引入Promise polfyfill
无构建型项目。不使用babel/TS编译器的项目可以直接使用这种方式
缺点
代码可维护性差
因js里的函数可以当参数传递,所以传入多少层都可以。这代表着当异步调用较多时会导致代码成了一坨缩进????山
难以理解
当一个异步操作可能需要执行有不同的回调时,代码理解成本就以几何倍数增长起来了。
Promise then阶段
Promise对象自带.then和.catch函数来传入一个执行success和fail的回调。
可以有多个.then串在一起挨个执行,但如果.then里有异步操作,需要在.then里return 该Promise才能使得当前then链顺序执行。
常规用法
axios.get('/user', {
params: {
ID: 12345
}
})
.then(function (response) {
// 这里可以继续return Promise
console.log(response);
})
.catch(function (error) {
console.log(error);
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
优点
代码相对优雅。一定层度上解决了回调地狱的问题,面对较复杂的回调可以采用Promise的then链来解决
缺点
- 有一定的学习成本和理解成本,后期维护较长的Promise链较困难。
- Promise对异步的执行是依靠.then的链式调用实现的,但then传入的还是回调函数,决定了复杂的逻辑写在then里会越来越臃肿。
- 需要拆解为多个then进行长链式调用,这就注定了Promise在逻辑上没有完全解决异步优雅使用的问题。
- 在不同的异步调用链有一些异步调用中间链路重叠时,代码处理也会比较复杂。例如获取异步判断登陆,然后获取userInfo这样的例子。
async await阶段
async ES6入门教程
MDN async
H5与客户端交互的桥调用及Ajax都可以封装成一个返回Promise的函数。
export default JSApi(api, options ) => {
return new Promise((resolve, reject) => {
window.ucapi.invoke(api, {
...options,
success: resolve,
fail: reject
})
})
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
async函数是把异步函数当同步用,await后面是一个Promise的对象,等待该Promise对象resolve后继续往后执行,实际上是Generator的yield,然后执行next()。
const changeText = (text)=>{
document.querySelector('#app').innerHTML = text;
}
const requestApi = ()=>{
return new Promise((resolve,reject)=>{
// 模拟接口返回
setTimeout(function(){
const result = '异步接口返回的response'
resolve(result);
},1000)
})
}
async function setDomText (){
const text = await requestApi();
changeText(text)
}
setDomText()
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
优点
- 异步完全同步化
- 代码清晰,无大量的Promise Api,维护方便
- 使用简单,理解成本低
- 和函数最小作用原则一起使用,异步调用函数链路清晰
缺点
- 错误处理需要有一定的经验
- 需要一些polyfill来支持await语法糖 针对缺点1,在上面的ES6入门讲解里讲的非常清晰了。能正确的利用.catch,try catch和 if语句能很好的控制await的返回和错误处理。
针对错误2,在支持Promise的环境,也只需要37行函数的兼容就可以了,其实性能损失并没有那么大,但带来的收益却非常大。
实例分析
抽取竞速游戏的一个较为典型的登陆获取用户信息案例进行分析,实际的代码比这要复杂的多。
export const loginThenGetUser = () => {
return new Promise((resolve, reject) => {
getUserInfo()
// 用户已登录
.then((args) => {
resolve(args);
})
// 用户未登录
.catch(() => {
login()
.then(() => {
getUserInfo()
.then((args) => {
resolve(args);
})
.catch(() => {
reject();
});
})
.catch(() => {
reject();
});
});
});
};
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
可以看到,上面的函数已经地狱回调了。
修改为await如下
// 名字还可以再斟酌
const forceLoginAndGetUserInfo = async ()=>{
const loginInfo = await login().catch(err=>{
// 控制登陆失败的情况
})
if(loginInfo){
// 登陆成功
return await getUserInfo();
}
}
const loginThenGetUser = async ()=>{
let userInfo;
try {
userInfo = await getUserInfo()
} catch (error) {
// 移交另一个函数处理
return forceLoginAndGetUserInfo()
}
// 已经登陆
return userInfo
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
无论是login 还是getUserInfo 都是异步操作,封装成最小的异步调用函数,业务逻辑在调用时比.then清晰非常多,个人理解,说的不对地方还望大佬多多指教????????????