JS的线程只有一个,而这个线程中拥有唯一的一个事件循环,JS代码的执行过程中,除了依靠词法作用域阶段就已经定好的函数调用栈来确定执行顺序之外,还会依靠任务队列(task queue)确定另外一些代码的执行。任务队列的顺序是先入先出(First-In-First-Out),虽然事件循环
只有一个,但是任务队列有多个。任务队列又分为了macro-task(宏任务)和micro-task(微任务)。
- macro-task(宏任务)包括
- script
- setTimeout
- setInterval
- setImmediate
- I/O
- UI rendering
- micro-task(微任务)包括
- process.nextTick
- Promise
- MutationObserver(html5新特性)
setTimeout/Promise等称之为任务源。而进入任务队列的是他们指定的具体执行任务。对于setTimeout来说它的回调函数才是进入任务队列的task,它只是一个任务的分发器,分发任务是即时的,但是回调函数的执行是延迟异步执行的。来自不同任务源的任务会进入到不同的任务队列中。延迟性定时器和间隔性定时器是同源的任务。
来看一个实际的例子:
1 | setTimeout(function() { |
总结起来其实就是一句话,同步优先,异步靠边,回调垫底。下面分析具体过程:
时间循环从宏任务队列的script开始,此时的函数调用栈是全局上下文栈,接下来就是遇到setTimerout函数,它会把任务分发给它的回调函数中,并在setTimeout队列中等待。
接下来遇到Promise实例,这是一个同步的构造函数,并不属于任何队列,直接执行。所以最先打出来的也是promise1。在它之后的then会被分发到micro-task(微任务)的Promise队列里等待执行,then可以看成是一个异步的回调。
接着继续看for循环也是同步,所以依次执行,到了console.log(promise2)。
然后调用栈的控制权交回给script,此时全局作用域下还剩下console.log(‘global1’),执行。
第一个宏任务script执行结束后开始执行所有的微任务,此时微任务还剩Promise队列里的任务then1,执行之后得到结果。
6.当所有的微任务执行结束后,第一轮的事件循环就结束了,开始第二轮的循环。第二轮循环仍然从宏任务macro-task开始。此时只有在setTimeout队列中的timeout1的任务等待执行。因此就直接执行即可。
至此宏任务队列和微任务队列都没有任务可执行了,则调用栈退出,执行环境销毁。
最终的结果就是:promise1 promise2 global1 then1 timeout1
小结:
事件循环从宏任务(macro-task)script进入,函数调用栈从全局开始,遇到的是同步,直接执行,不会添加到队列中(循环、语句、Function对象实例的函数),遇到异步的微任务(micro-task)则加入所属的队列中等待执行,异步程序包括(promise的then回调、process.nextTick),当前执行环境下,所有的同步执行完毕,异步也排好队以后,第一次的宏任务script执行完毕时,接着开始将队列中等待执行的微任务执行完,至此第一轮的事件循环结束。第二轮循环仍然从宏任务开始,这一步里可能有还在队列中等待的回调函数,将其执行结束后,所有的任务执行完毕,循环结束。