简介
javascript的一大特点就是单线程,这个线程中拥有唯一的一个事件循环。事件循环机制从整体上告诉了我们javascript代码的执行顺序。javascript代码的执行过程中,除了一开函数调用栈来搞定函数的执行顺序外,还依靠任务队列(task queue)来搞定另外一些代码的执行。
一个线程中,事件循环是唯一的,但是任务队列可以拥有多个。
任务队列
宏任务(macro-task)
script(整体代码),setTimeOut,setInterval,setImmediate,I/O,UI rendering
微任务(micro-task)
process.nextTick,promise,MutationObserver
任务源
setTimeOut/Promise称为任务源,而进入队列的是他们制定的执行任务。
规则
- 来自不同任务源的 任务会进入到不同的任务队列,其中setTimeout与setInterval是同源的。
- 事件循环的顺序,决定了javascript代码的执行顺序。它从script(整体代码)开始第一次循环。全局上下文进入函数调用栈,直到调用栈清空(只剩下全局),然后执行所有micro-task;当所有micro-task执行完毕之后,循环再次从macro-task开始,找到其中一个macro任务队列进行执行,执行完毕,再执行所有micro-task;这样一直循环下去。
- 其中每一个任务的执行,无论是macro-task还是micro-task,都是借用函数调用栈来完成的。
例子
1 | setTimeout(function() { |
首先,事件循环从宏任务队列开始,这时候,宏任务队列中,只有一个script(整体代码)任务。每一个任务的执行顺序,都依赖函数调用栈来搞定,而当遇到任务源时,则会先分发任务到对应的队列中,所以上面的例子的第一步执行如下所示,script任务开始执行,全局上下文入栈:
第二步:script任务执行时首先遇到了setTimeout,setTimeout为一个宏任务源,那么他的作用就是将任务分到它对应的队列中。
1 | setTimeout(function() { |
宏任务timeout1进入setTimeout队列。
第三步,script执行时,遇到Promise实例,Promise构造函数中的第一个参数,是在new的时候执行,因此不会进入到任何其他队列,而是直接在当前任务直接执行了,而后续的then则会分配到微任务的Promise队列中。
因此,构造函数执行时,里面的参数进入函数调用栈,for循环不会进入任何队列,因此代码会依次执行,所以这里的promise1和promise2会依次输出。
promise1入栈执行,这时promise1被最先输出。
resolve在for循环中入栈执行。
构造函数执行完毕的过程中,resolve执行完毕出栈,promise2输出,promise也出栈,then执行时,Promise任务then1进入对应的微任务队列。
script任务继续往下执行,最后只输出了一句global1,然后全局任务就执行完毕了。
第四步:第一个宏任务script执行完毕后,就开始执行所有的可执行的任务。这个时候,微任务队列中,只有promise队列中的一个任务then1,因此直接执行,结果输出then1。当然,也是进入函数调用栈中执行的。
第五步:当所有的微任务执行完毕之后,表示第一轮循环结束了。这个时候进入第二轮循环,第二轮循环仍从宏任务macro-task开始。
这个时候,发现宏任务中,只有setTimeout队列中还有一个timeou1的任务等待执行,因此直接执行即可。
这时宏任务与微任务队列中都没有任务了,因此不会再输出其他东西了。