详解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 相关文章推荐
JavaScript Event学习第八章 事件的顺序
Feb 07 Javascript
javascript实现复制与粘贴操作实例
Oct 16 Javascript
js实现发送验证码后的倒计时功能
May 28 Javascript
JS实现的最简Table选项卡效果
Oct 14 Javascript
JavaScript使用DeviceOne开发实战(一) 配置和起步
Dec 01 Javascript
jquery简单倒计时实现方法
Dec 18 Javascript
jQuery实现图片上传预览效果功能完整实例【测试可用】
May 28 jQuery
react native 获取地理位置的方法示例
Aug 28 Javascript
H5+C3+JS实现双人对战五子棋游戏(UI篇)
May 28 Javascript
JS高阶函数原理与用法实例分析
Jan 15 Javascript
jQuery-Citys省市区三级菜单联动插件使用详解
Jul 26 jQuery
koa2的中间件功能及应用示例
Mar 05 Javascript
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
php addslashes及其他清除空格的方法是不安全的
2012/01/25 PHP
解析php中mysql_connect与mysql_pconncet的区别详解
2013/05/15 PHP
深入php函数file_get_contents超时处理的方法详解
2013/06/03 PHP
php+mysql实现无限级分类
2015/11/11 PHP
PHP+redis实现微博的拉模型案例详解
2019/07/10 PHP
javascript 兼容鼠标滚轮事件
2009/04/07 Javascript
C#中TrimStart,TrimEnd,Trim在javascript上的实现
2011/01/17 Javascript
js 实现 input type="file" 文件上传示例代码
2013/08/07 Javascript
ExtJS判断IE浏览器类型的方法
2014/02/10 Javascript
Jquery跳到页面指定位置的方法
2014/05/12 Javascript
全面解析Bootstrap表单使用方法(表单控件)
2015/11/24 Javascript
JS三级可折叠菜单实现方法
2016/02/29 Javascript
Angular2  NgModule 模块详解
2016/10/19 Javascript
bootstrap table配置参数例子
2017/01/05 Javascript
基于vue.js轮播组件vue-awesome-swiper实现轮播图
2017/03/17 Javascript
详解angular中的作用域及继承
2017/05/31 Javascript
通过javascript实现段落的收缩与展开
2019/06/26 Javascript
Cordova(ionic)项目实现双击返回键退出应用
2019/09/17 Javascript
茶余饭后聊聊Vue3.0响应式数据那些事儿
2019/10/30 Javascript
vue限制输入框只能输入8位整数和2位小数的代码
2019/11/06 Javascript
Python编程之Re模块下的函数介绍
2017/10/28 Python
Python实现列表删除重复元素的三种常用方法分析
2017/11/24 Python
Python决策树分类算法学习
2017/12/22 Python
python使用phoenixdb操作hbase的方法示例
2019/02/28 Python
Pandas透视表(pivot_table)详解
2019/07/22 Python
python 修改本地网络配置的方法
2019/08/14 Python
python框架flask入门之环境搭建及开启调试
2020/06/07 Python
scrapy实践之翻页爬取的实现
2021/01/05 Python
苏格兰在线威士忌商店:The Whisky Barrel
2019/05/07 全球购物
Lentiamo荷兰:在线订购隐形眼镜、隐形眼镜液和太阳镜
2019/10/25 全球购物
软件测试题目
2013/02/27 面试题
2014年房地产个人工作总结
2014/12/20 职场文书
财产保全担保书
2015/01/20 职场文书
大学生就业推荐表自我评价
2015/03/02 职场文书
Python  lambda匿名函数和三元运算符
2022/04/19 Python
spring boot实现文件上传
2022/08/14 Java/Android