快速导航
前言
最近又碰到了一个有意思的题。其代码如下:
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout0')
},0)
setTimeout(function(){
console.log('setTimeout3')
},3)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
console.log('script end')
我们都知道JS是按照段来执行的,所以按照我们平时的预期那么应该输出:
script start->setTimeout0->async1 start->async2-> async1 end->promise1->promise3->promise2->script end
chrome浏览器下实际输出
script start->async1 start->async2->promise1->promise2 ->script end->async1 end->promise3->setTimeout0->setTimeout3
实际情况和我们的预估相差甚远啊。那是什么原因导致的呢?接下来我们好好的学习一下导致这种原因。 学习其内部执行的过程,你就明白上述输出是怎么得来的。
JS代码的执行流程
我们都知道JS引擎是从上到下顺序的进行执行的,每一个可执行代码,最终将会被排成任务(Task),然后放入队列中,然后顺序的被执行。接下来我将分两个阶段来讲述JS代码的执行流程,这两个阶段如下:
- 可执行队列生成阶段
- 事件循环
执行队列生成阶段
;
该流程主要是根据代码生成宏任务队列和微任务队列。 如上图所示,可执行队列的生成步骤如下:
-
当程序执行开始,遇到可执行代码,会判断该
可执行代码(也可叫任务)是否是同步的,如果是,则将其压入执行栈,如果不是则执行步骤2; -
当该任务是异步任务的时候,也即步骤1判断为非同步任务时。则在事件列表中注册该异步任务的回调函数;
-
等待满足回调函数执行条件后,然后判断该任务是宏任务还是微任务,若是宏任务,则将该回调函数加入宏任务队列,若是微任务,则加入为任务队列。
注意:这里补充几个概念。
- 任务(Task) : 就是js的可执行代码,例如:示例中的
setTimeout(function(){
console.log('setTimeout0')
},0)
等,非声明代码都可被称为任务。JS的任务由宏任务和微任务组成。
- 宏任务:一般来源于setTimeout 、setInterval、setImmediate、I/O操作、UI rendering的任务。
- 微任务:相比于宏任务来说,微任务来源目前只有:process.nextTick、promise中的then\catch\finally方法、Object.observe、MutationObserve四种。
- 宏任务队列和微任务队列:其实就是
队列(FIFO),只不过装宏任务的叫宏任务队列,微任务的叫微任务队列。
注意:promise.then,promise.catch,promise.finally才是微任务。排除微任务就是宏任务啦。对于异步任务一定是要满足执行条件后才能将回调函数作为任务加入队列。
事件循环
事件循环可以理解为一个while死循环,他会重复的循环执行循环体内的代码。
其执行步骤如下:
-
判断执行栈是否执行完栈内任务,若没有则继续执行栈内任务,若执行完则进行步骤2。注意:执行栈和事件循环是独立的,不存在相互依赖,事件循环里的任务最终要被推到执行栈执行。
-
判断宏任务队列是否为空,如为不为空,则执行宏任务队列中放入时间最久的那个任务,然后将他从队列中移除,然后执行步骤3的内容。若队列为空则直接执行步骤2的内容。
-
判断微任务队列是否为空,若为空则执行步骤4。若不为空,则循环执行微任务队列中的每一个任务并将其移除,直到其为空为止,然后执行步骤4。
-
更新渲染,这步包括:html标签解析为DOM树,css解析为CSSDOM树,然后合成渲染树,然后根据渲染树布局,计算出节点几何信息,最后将各节点绘制在屏幕上。然后回到步骤1,再一次执行。
注意:事件循环的每一次循环只执行一个宏任务,循环执行所有微任务,更新一次渲染。每个事件循环可以有多个宏任务队列,但是只有一个微任务队列。
小结:JS引擎遇到可执行代码时,首先会根据任务为宏任务还是微任务,分别加入宏任务队列和微任务队列。然后事件循环执行相应的任务代码,当再次遇到新任务时,再次判断其新任务类型,然将该任务加入队列执行。
实践分析
接下来,我们一边复习之前学习的流程,一边分析前言的那道题的执行流程。
-
首先我们遇到的是将整个代码作为一个宏任务加入宏任务队列。然后被压入执行栈执行。因为现在执行栈不为空,所以不会讲新的任务压入执行栈
-
然后遇到同步任务console.log('script start') 将其压入执行栈执行,这里执行后会输出'script start',执行后将该任务弹出执行栈;
-
然后遇到 setTimeout(function(){console.log('setTimeout0') },0);因为其等待时间到了,将其回调函数加入宏队列;
-
然后遇到 setTimeout(function(){console.log('setTimeout3') },3);其等待时间未到,暂不做处理;
-
然后遇到async1(); 其是同步任务,直接压入执行栈执行,其执行有遇到 console.log('async1 start')压入执行栈,执行后输出async1 start;执行后将该任务弹出执行栈;又遇到async2()同步任务(这里虽然有await,但是是在执行完async2()后才执行await的)压入执行栈,执行时遇到console.log('async2')同步任务,压入执行栈 ,输出async2,然后弹出该任务,弹出async2任务。遇到await直接在微任务队列添加一个微任务promise.then的回调函数(ps:引用阮一峰老师的一句话:"async 函数返回一个 Promise 对象,当函数执行的时候,一旦遇到 await 就会先返回,等到触发的异步操作完成,再接着执行函数体内后面的语句。");
-
然后遇到
new Promise(function(resolve){
console.log('promise1')
resolve();
console.log('promise2')
}).then(function(){
console.log('promise3')
})
这里需要注意一下,new Promise(function(){})这个是同步任务,只有promise.then,promise.catch,promise.finally才是异步微任务。所以直接压入执行栈执行。执行时遇到console.log('promise1'),压入执行栈执行,然后输出promise1。遇到resolve();将promise.then的回调函数加入微任务队列。然后遇到 console.log('promise2'),压入执行栈中执行,输出promise2;
-
然后遇到 console.log('script end'),压入执行栈,输出script end;
-
现在整个代码作为一个宏任务被执行完了,现在开始微任务队列里的任务进行压入执行。在之前分析的时候,我们在第5步中将await后的任务作为一个微任务promise.then的回调函数压入微任务队列,然后在第6步的时候压入promise.then(function(){ console.log('promise3')}) ;的回调函数。所以微任务队列里有两个任务。首先将第一个promise.then()的回调函数压入执行栈执行,然后遇到 console.log('async1 end')压入执行栈执行,输出async1 end。然后将第二个promise.then的回调函数压入执行栈执行,遇到console.log('promise3'),压入执行栈,输出promise3。
-
在上一步中执行完微任务,检测到执行栈为空,所以讲宏任务队列的第一个宏任务压入执行栈,也就是setTimeout(function(){console.log('setTimeout0') },0)回调函数执行,然后遇到console.log('setTimeout0'),压入执行栈执行,输出setTimeout0;此时微任务队列为空,执行下一个宏任务
-
当setTimeout(function(){console.log('setTimeout3') },3)时间到了,然后将其回调函数加入宏任务队列,然后压入执行栈执行,遇到console.log('setTimeout3'),直接压入执行栈执行,输出setTimeout3;
综上:script start -> async1 start -> async2 -> promise1 -> promise2 -> script end -> async1 end -> promise3 -> setTimeout0 -> setTimeout3
总结
本文主要简述了代码的一个整体的执行流程,主要有必要队列(宏,微)的生成、判定,以及简化的事件循环流程,仔细详看后一定会有一点的收获。当然本文也有很多瑕疵,美中不足的地方,请多多包涵。
最后内容难免出错,欢迎指正,交流。