曾经见过为了让钩子函数的异步代码可以同步执行,而对钩子函数使用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
这个打印顺序有没有让你想到什么?!
任务队列!
我们都知道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),有即出队执行。