详解vue中async-await的使用误区


Posted in Javascript onDecember 05, 2018

曾经见过为了让钩子函数的异步代码可以同步执行,而对钩子函数使用async/await,就好像下面的代码:

// exp-01
export default {
 async created() {
 const timeKey = 'cost';
 console.time(timeKey);
 console.log('start created');
 this.list = await this.getList();
 console.log(this.list);
 console.log('end created');
 console.timeEnd(timeKey);
 },
 mounted() {
 const timeKey = 'cost';
 console.time(timeKey);
 console.log('start mounted');
 console.log(this.list.rows);
 console.log('end mounted');
 console.timeEnd(timeKey);
 },
 data() {
 return {
  list: []
 };
 },
 methods: {
 getList() {
  return new Promise((resolve) => {
  setTimeout(() => {
   return resolve({
   rows: [
    { name: 'isaac', position: 'coder' }
   ]
   });
  }, 3000);
  });
 }
 }
};

exp-01 的代码最后会输出:

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 3171.545166015625ms

很明显没有达到预期的效果,为什么?

根据 exp-01 的输出结果,可以看出代码的执行顺序,首先是钩子的执行顺序:

created => mounted

是的,钩子的执行顺序还是正常的没有被打乱,证据就是:created钩子中的同步代码是在mounted先执行的:

start created
start mounted

再看看created钩子内部的异步代码:

this.list = await this.getList();

可以看见this.list的打印结果

end mounted
mounted cost: 2.88623046875ms
// 这是created钩子打印的this.list
{__ob__: Observer}
end created

在mounted钩子执行完毕之后才打印,言外之意是使用async/await的钩子内部的异步代码并没有起到阻塞钩子主线程的执行。这里说的钩子函数的主线程是指:

beforeCreate => created => beforeMount => mounted => ...

会写出以上代码的原因我估计有两个:

exp-01

正文

剖析一下

前言中针对代码的执行流程分析了一下,很明显没有如期望的顺序执行,我们先来回顾一下期望的顺序是什么

// step 1
created() {
 // step 1.1
 let endTime;
 const startTime = Date.now();
 console.log(`start created: ${startTime}ms`);
 // step 1.2
 this.list = await this.getList();
 endTime = Date.now();
 console.log(this.list);
 console.log(`end created: ${endTime}ms, cost: ${endTime - startTime}ms`);
},
// step 2
mounted() {
 let endTime;
 const startTime = Date.now();
 console.log(`start mounted: ${startTime}ms`);
 console.log(this.list.rows);
 endTime = Date.now();
 console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
}

// step 1 => step 1.1 => step 1.2 => step 2

期望的打印结果是:

// step 1(created)
start created
// this.list
{__ob__: Observer}
end created
created cost: 3171.545166015625ms

// step 2(mounted)
start mounted
// this.list.rows
[{…}, __ob__: Observer]
end mounted
mounted cost: 2.88623046875ms

对比实际的打印和期望的打印,就知道问题出在created钩子内使用了await的异步代码,并没有达到我们期望的那种的“异步代码同步执行”的效果,仅仅是一定程度上达到了这个效果。

下面来分析一下为什么会出现这个非预期的结果!

在分析前,让我们来回顾一下一些javascript的基础知识!看看下面这段代码:

(function __main() {
 console.log('start');
 setTimeout(() => {
 console.log('console in setTimeout');
 }, 0);
 console.log('end');
})()

// output
start
end
console in setTimeout

这个打印顺序有没有让你想到什么?!

任务队列!

详解vue中async-await的使用误区

我们都知道JavaScript的代码可以分成两类:

同步代码 和 异步代码

同步代码会在主线程按照编写顺序执行;

异步代码的触发过程(注意是触发,比如异步请求的发起,就是在主线程同步触发的)是同步的,但是异步代码的实际处理逻辑(回调函数)则会在异步代码有响应时将处理逻辑代码推入任务队列(也叫事件队列),浏览器会在主线程(指当前执行环境的同步代码)代码执行完毕后以一定的周期检测任务队列,若有需要处理的任务,就会让队头的任务出队,推入主线程执行。

比如现在我们发起一个异步请求:

// exp-02
console.log('start');
axios.get('http://xxx.com/getList')
 .then((resp) => {
 console.log('handle response');
 })
 .catch((error) => {
 console.error(error);
 });
console.log('end');

在主线程中,大概首先会发生如下过程:

// exp-03
// step 1
console.log('start');

// step 2
axios.get('http://xxx.com/getList'); // 此时回调函数(即then内部的逻辑)还没有被调用

// step 3
console.log('end');

在看看浏览器此时在干什么!

此时事件轮询(Event Loop)登场,其实并非此时才登场,而是一直都在!

“事件轮询”这个机制会以一定的周期检测任务队列有没有可执行的任务(所谓任务其实就是callback),有即出队执行。

当 step 2 的请求有响应了,异步请求的回调函数就会被添加到任务队列(Task Queue)或者 称为 事件队列(Event Queue),然后等到事件轮询的下一次检测任务队列,队列里面任务就会依次出队,进入主线程执行:即执行下面的代码:

// 假定没有出错的话
((resp) => {
 console.log('handle response');
})()

到此,简短科普了任务队列的机制,联想 exp-01 的代码,大概知道出现非预期结果的原因了吧!

created钩子中的await函数,虽然是在一定程度上是同步的,但是他还是被挂起了,实际的处理逻辑(this.list =resp.xxx)则在响应完成后才被添加进任务队列,并且在主线程的同步代码执行完毕后执行。 下面是将延时时间设为0后的打印:

start created
start mounted
undefined
end mounted
mounted cost: 2.88623046875ms
{__ob__: Observer}
end created
created cost: 9.76611328125ms

这侧面说明了await函数确实被被挂起,回调被添加到任务队列,在主线程代码执行完毕后等待执行。

然后是为什么说 exp-01 的代码是一定程度的同步呢?!

同步执行的另一个意思是不是就是:阻塞当前线程的继续执行直到当前逻辑执行完毕~

看看 exp-01 的打印:

{__ob__: Observer}
end created
created cost: 3171.545166015625ms

end created 这句打印,是主线程的代码,如果是一般的异步请求的话,这句打印应该是在 {__ob__: Observer} 这句打印之前的yo,至于为什么会这样,这里就不多解析,自行google!

另外,这里来个小插曲,你应该注意到,我一直强调,回调函数被添加进任务队列的时机是在响应完成之后,没错确实如此的!

但在不清除这个机制前,你大概会有两种猜想:

1.在触发异步代码的时,处理逻辑就会被添加进任务队列;
2.上面说到的,在异步代码响应完成后,处理逻辑才会被添加进任务队列;

其实大可推断一下

队列的数据结构特征是:先进先出(First in First out)

此时假如主线程中有两个异步请求如下:

// exp-04
syncRequest01(callback01);
syncRequest02(callback02);

假设处理机制是第一点描述那样,那么callback01就会先被添加进任务队列,然后是callback02。

然后,我们再假设syncRequest01的响应时间是10s,syncRequest02的响应时间是5s。

到这里,有没有察觉到违和感!

异步请求的实际表现是什么?是谁快谁的回调先被执行,对吧!那么实际表现就是callback02会先于callback01执行!

那么基于这个事实,再看看上面的假设(callback01会执行)~

ok!插曲完毕!

解法

首先让我回顾一下目的,路由组件对异步请求返回的数据有强依赖,因此希望阻塞组件的渲染流程,待到异步请求响应完毕之后再执行。

这就是我们需要做的事情,需要强调的一点是: 我们对数据有强依赖 ,言外之意就是数据没有按预期返回,就会导致之后的逻辑出现不可避免的异常。

接下来,我们就需要探讨一下解决方案!

组件内路由守卫了解一下!?

beforeRouteEnter
beforeRouteUpdate (2.2 新增)
beforeRouteLeave

这里需要用到的路由守卫是: beforeRouterEnter , 先看代码:

// exp-05
export default {
 beforeRouteEnter(to, from, next) {
 this.showLoading();
 this.getList()
  .then((resp) => {
  this.hideLoading();
  this.list = resp.data;
  next();
  })
  .catch((error) => {
  this.hideLoading();
  // handle error
  });
 },

 mounted() {
 let endTime;
 const startTime = Date.now();
 console.log(`start mounted: ${startTime}ms`);
 console.log(this.list.rows);
 endTime = Date.now();
 console.log(`end mounted: ${endTime}ms, cost: ${endTime - startTime}ms`);
 },
};

路由守卫 beforeRouterEnter ,触发这个钩子后,主线程都会阻塞,页面会一直保持假死状态,直到在调用 beforeRouterEnter 的回调函数 next ,才会跳转路由进行新路由组件的渲染。

看起这个解决方案相当适合上面我们提出的需求,在调用 next 前,就可以去拉取数据!

但是如刚刚说到的,页面在一直假死,加入数据获取花费时间过长就难免变得很难看,用户体验未免太差

为此,在 exp-05 中我在请完成前后分别调用了 this.showLoading() 和 this.hideLoading() 以便页面 keep-alive 。

这个处理假死的loading有没有让你想到写什么,没错就是下面这个github跳转页面是顶部的小蓝条

详解vue中async-await的使用误区

想想就有点cool,当然还有很多的实现方式提升用户体验,比如作为body子元素的全屏loading,或者button-loading等等……

当然,我们知道阻塞主线程怎么都是阻塞了,loading只是一种自欺欺人式的优化(此时这个成语可不是什么贬义的词语)!

因此,不是对数据有非常强的依赖,都应在路由的钩子进行数据抓取,这样就可以让用户“更快”地跳转到目的页。为避免页面对数据依赖抛出的异常(大概就是 undefined of xxx ),我们可以对初始数据进行一些预设,比如 exp-01 中对 this.list.rows 的依赖,我们可以预设 this.list :

list: {
 rows: []
}

这样就不会抛出异常,待到异步请求完成,基于vue的update机制二次渲染我们的预期数据~

小结

对于 exp-01 的写法,也不能说他是错误或不好的写法,凡事都要看我们是出于什么目的,如果仅仅是为了保证多个异步函数的执行顺序, exp-01 的写法没有任何错误,因此async/await不能用在路由钩子上什么的并不存在!

Javascript 相关文章推荐
js中的string.format函数代码
Aug 11 Javascript
window.parent与window.openner区别介绍
Apr 12 Javascript
JQuery与JS里submit()的区别示例介绍
Feb 17 Javascript
前端弹出对话框 js实现ajax交互
Sep 09 Javascript
javascript设置文本框光标的方法实例小结
Nov 04 Javascript
Vue.js基础知识小结
Jan 13 Javascript
vue实现点击图片放大效果
Aug 15 Javascript
React Native 环境搭建的教程
Aug 19 Javascript
seajs中最常用的7个功能、配置示例
Oct 10 Javascript
详解如何用typescript开发koa2的二三事
Nov 13 Javascript
javascript实现手动点赞效果
Apr 09 Javascript
Vue项目中使用jquery的简单方法
May 16 jQuery
Vue中的基础过渡动画及实现原理解析
Dec 04 #Javascript
使用FormData实现上传多个文件
Dec 04 #Javascript
vue自定义指令的创建和使用方法实例分析
Dec 04 #Javascript
用vuex写了一个购物车H5页面的示例代码
Dec 04 #Javascript
vue实现的双向数据绑定操作示例
Dec 04 #Javascript
使用jquery模拟a标签的click事件无法实现跳转的解决
Dec 04 #jQuery
jQuery利用FormData上传文件实现批量上传
Dec 04 #jQuery
You might like
精致的人儿就要挑杯子喝咖啡
2021/03/03 冲泡冲煮
一个程序下载的管理程序(一)
2006/10/09 PHP
PHP中静态变量的使用方法实例分析
2016/12/01 PHP
PHP编程实现计算抽奖概率算法完整实例
2017/08/09 PHP
显示、隐藏密码
2006/07/01 Javascript
图片上传即时显示缩略图的js代码
2009/05/27 Javascript
获取数组中最大最小值方法js代码(自写)
2013/08/12 Javascript
Function.prototype.bind用法示例
2013/09/16 Javascript
JS连连看源码完美注释版(推荐)
2013/12/09 Javascript
jQuery中scrollLeft()方法用法实例
2015/01/16 Javascript
javascript实现树形菜单的方法
2015/07/17 Javascript
JS模拟的Map类实现方法
2016/06/17 Javascript
js/jquery控制页面动态加载数据 滑动滚动条自动加载事件的方法
2017/02/08 Javascript
easy ui datagrid 从编辑框中获取值的方法
2017/02/22 Javascript
微信小程序动态显示项目倒计时效果
2017/06/13 Javascript
Webpack常见静态资源处理-模块加载器(Loaders)+ExtractTextPlugin插件
2017/06/29 Javascript
Vue resource中的GET与POST请求的实例代码
2017/07/21 Javascript
使用JQuery实现图片轮播效果的实例(推荐)
2017/10/24 jQuery
了解ESlint和其相关操作小结
2018/05/21 Javascript
Vue源码中要const _toStr = Object.prototype.toString的原因分析
2018/12/09 Javascript
es6数值的扩展方法
2019/03/11 Javascript
如何使用pm2快速将项目部署到远程服务器
2019/03/12 Javascript
javascript使用substring实现的展开与收缩文字功能示例
2019/06/17 Javascript
[02:41]2015国际邀请赛中国区预选赛观战指南
2015/05/20 DOTA
Python实现扫描指定目录下的子目录及文件的方法
2014/07/16 Python
Java分治归并排序算法实例详解
2017/12/12 Python
浅析PHP与Python进行数据交互
2018/05/15 Python
Python实现的redis分布式锁功能示例
2018/05/29 Python
Django框架创建mysql连接与使用示例
2019/07/29 Python
python如何进行矩阵运算
2020/06/05 Python
N:Philanthropy官网:美国洛杉矶基础款服装
2020/06/09 全球购物
活动邀请函范文
2014/01/19 职场文书
2015新年联欢晚会开场白
2014/12/14 职场文书
检讨书格式
2015/01/23 职场文书
社区端午节活动总结
2015/02/11 职场文书
工作报告范文
2019/06/20 职场文书