静下心学了一波事件循环机制,好开心,我学会了,首先还是得感谢作者写的笔记特别详细 链接: http://www.cnblogs.com/lsgxeva/p/7976217.html
javascript的特点之一就是单线程,而这个线程拥有唯一的一个事件循环.javascript代码在执行过程中,除了依靠函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行.
队列数据结构
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个.
任务队列分为macro-task(宏任务)与micro-task(微任务),
macro-task大概包括: script(整体代码),setTimeout,setInterval,setImmediate,I/o, UI rendering
micro-task大概包括: process.nextTick, promise, object.observe(已废弃) mutationObserver(html5新特性).
setTimeout/Promise等我们称之为任务源.而进行任务队列的是他们指定的具体执行任务.
来自不同任务源的任务会进入到不同的任务队列.其中setTimeout与setInterval是同源.
事件循环的顺序,决定了javascript代码的执行顺序.它从script(整体代码)开始第一次循环.之后全局上下文进入函数调用栈.直到函数调用栈(只剩全局),然后执行所有的micro-task.当所有可执行的micro-task执行完毕之后.循环再次从macro-task开始,找到其中一个任务队列执行完毕,然后再执行所有的micro-task,这样一直循环下去
以下举个例子:
首先,事件循环从宏任务队列开始,宏任务队列中,只有一个script(整体代码)任务.每一个任务的执行顺序,都依靠函数调用栈来搞定,而当遇到任务源时,则会先分发任务到相应的队列中去,所以第一步如下图
首先script任务开始执行,全局上下文入栈,第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分发到它对应的队列中.
第三步: script执行时遇到promise实例.promise构造函数中的第一个参数,是在new的时候执行,因此不会进入任何其他的队列,而是直接在当前任务直接执行了.而后续的.then则会分发到micro-task的promise队列中去.
因此,构造函数执行时,里面的参数进入函数调用栈执行.for循环不会进行任何队列,因此代码依次执行,所以这里的promise1和promise2会依次输出.
构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise1页出栈,then执行时,promise任务then1进入对应队列.
script任务继续往下执行,最后只有一句输出global1,然后,全局任务就执行完毕了.
第四步: 第一个宏任务script执行完毕之后,就开始执行所有的可执行的微任务.这个时候,微任务中,只有promise队列中的一个任务then1,因此直接执行就行了,执行结果输出then1,当然,他的执行也是进入函数调用栈中执行的
第5步:当所有的micro-task执行完毕之后,表示第一轮的循环就结束了.这个时候就得开始第二轮的循环.第二轮循环仍然从宏任务macro-task开始
这个时候,我们发现宏任务中,只有setTimeout队列中还要一个timeout的任务等待执行.因此就直接执行即可.
这个时候宏任务队列与微任务队列中都没有任务了,所以代码就不会在输出其他东西了.下面有一个更加难的栗子,帮助更加的理解.
// demo02console.log('golb1');setTimeout(function() {console.log('timeout1'); process.nextTick(function() {console.log('timeout1_nextTick'); })newPromise(function(resolve) {console.log('timeout1_promise'); resolve(); }).then(function() {console.log('timeout1_then') })})setImmediate(function() {console.log('immediate1'); process.nextTick(function() {console.log('immediate1_nextTick'); })newPromise(function(resolve) {console.log('immediate1_promise'); resolve(); }).then(function() {console.log('immediate1_then') })})process.nextTick(function() {console.log('glob1_nextTick');})newPromise(function(resolve) {console.log('glob1_promise'); resolve();}).then(function() {console.log('glob1_then')})setTimeout(function() {console.log('timeout2'); process.nextTick(function() {console.log('timeout2_nextTick'); })newPromise(function(resolve) {console.log('timeout2_promise'); resolve(); }).then(function() {console.log('timeout2_then') })})process.nextTick(function() {console.log('glob2_nextTick');})newPromise(function(resolve) {console.log('glob2_promise'); resolve();}).then(function() {console.log('glob2_then')})setImmediate(function() {console.log('immediate2'); process.nextTick(function() {console.log('immediate2_nextTick'); })newPromise(function(resolve) {console.log('immediate2_promise'); resolve(); }).then(function() {console.log('immediate2_then')
})
})
第一步: 宏任务script首先执行.全局入栈.globl1输出.
第二步,执行过程遇到setTimeout.setTimeout作为任务分发器,将任务分发到对应的宏任务队列中.
第三部:执行过程遇到setImmediate setImmediate也是一个宏任务分发器,将任务分发到对应的任务队列中.setImmediate的任务队列在setTimeout队列的后面执行.
第四步: 执行遇到nextTick,process.nextTick是一个微任务分发器,它会将任务分发到对应的微任务队列中去.
第五步:执行遇到promise.promise的then方法会将任务分发到对应的微任务队列中,但是它构造函数中的方法直接执行.因此,globl_promise会第二输出.
第六步: 执行遇到第二个setTimeout
第七步:先后遇到nextTick与Promise
第八步:再次遇到setImmediate
这个时候,script中代码就执行完毕了,执行过程中,遇到不同的任务分发器,就将任务分发到各自对应的队列中去,接下来,将会执行所有的微任务队列中的任务.
其中,nextTick队列会比promise先执行.nextTick中的可执行任务完毕之后,才会开始执行promise队列中任务,当所有可执行的微任务执行完毕之后,这一轮循环就表示结束了.下一轮循环继续从宏任务队列开始执行.这个时候,script已经执行完毕,所以就从setTimeout队列开始执行.
setTimeout任务的执行,也是依然是借助函数调用栈来完成,并且遇到任务分发器的时候也会将任务分发到对应的队列中去.
只有当setTimeout中所有的任务执行完毕之后,才会再次开始执行微任务队列.并且清空所有的可执行微任务.
大家需要注意这里的循环结束的时间节点
当我们执行setTimeout任务中遇到setTimeout时,它仍然会将对应的任务分发到setTimeout队列中去,但是该任务就得到等到下一轮事件循环执行了.栗子中还没有涉及到这么复杂的嵌套,可以动手添加或者修改他们的位置感受一下.
总结到这.