事件环(Event loop)是什么?
首先我们都知道JS是单线程的,同一时刻只能做一件事。所有的任务都要依次排队等待主线程的执行。
为了提高JS执行的效率,JS设计者提出了异步的概念,异步任务分为很多种,例如:定时器任务、ajax请求、I/O操作等。不同的异步任务执行的优先级是不一样的,Event loop就是在JS开始执行时,给不同的任务进行排队的一个机制。
了解Event loop之前需要知道的一些概念
-
进程与线程
进程是操作系统(cpu)分配资源的最小单位,线程是操作系统(cpu)调度任务的最小单位。
线程是建立在进程上的一次程序运行单位,一个进程上可以有多个线程。
在浏览器中我们最关注的渲染引擎属于进程。它包括GUI渲染线程、JS引擎线程、事件触发线程、定时触发器线程、异步http请求线程。

-
宏任务与微任务
macrotask(宏任务):可以理解是每次执行栈执行的代码就是一个宏任务。主要包括:主代码块、setTimeout、setInterval、setImmadiate(node的)、 I/O、UI rendering
microtask(微任务):可以理解是在当前 task 执行结束后立即执行的任务。主要包括:promise的then、process.nexttick(node的)、MutationObserve(浏览器端的)、messageChannel(浏览器端的)
浏览器中的Event loop
- 浏览器事件环原理
在浏览中各线程之间任务流转的关系图: 我们可以看到浏览器中有一个主线程(也就是JS引擎),JS开始执行的时候,所有的同步任务都放入主线程依次执行,形成一个主线程执行栈。
主线程之外,还存在两个’任务队列’,微任务队列和宏任务队列。只要异步任务有了结果,可以开始执行了,任务所在的内核的所属线程就会将任务放入其所属的任务队列中。
那么主线程执行栈和两个任务队列之间是怎么循环调度的呢?这也就是我们图中标红的部分。它的具体执行机制如下图所示:
- 首先JS开始执行,所有的同步任务都放入主线程执行栈中,依次开始执行。异步任务都放到各自的任务队列中等待执行。
- 一旦主线程执行栈中的任务都执行完毕,事件驱动机制会首先检查微任务队列,如果微任务队列中有等待中的任务,则将微任务队列中的任务全部取出,放入执行栈中依次执行。如果微任务队列中没有任务,则会从宏任务队列中取一个宏任务,放入执行栈中执行。
- 如果被放入主线程执行的任务中,发现有微任务或者宏任务,会将它们分配到各自的任务队列中,等待下一次事件循环中主线程来调用。
- 每一次主线程执行栈被清空后,都会重复2,3两步,直到所有的任务全部执行完毕。
-
代码示例
例1:console.log(1); setTimeout(function(){ console.log('setTimeout1') setTimeout(function(){ console.log('setTimeout1-1'); }) },0) setTimeout(function(){ console.log('setTimeout2'); setTimeout(function(){ console.log('setTimeout2-1'); }) },0) console.log(2)执行结果:

- 代码开始执行,console.log(1)和console.log(2)被放入执行栈中,外层的两个setTimeout通过定时触发器线程计时后放入宏任务任务队列。
- 执行栈中代码开始执行,依次输出1和2
- 执行栈为空,检查微任务队列没有任务可执行,然后检查宏任务队列,取出一个宏任务放入执行栈,开始执行,输出setTimeout1,然后发现有setTimeout异步任务,将其交给定时触发器线程计时后放入宏任务队列,此时宏任务队列中情况如下:
4.输出setTimeout1后,执行栈再一次被清空,于是再次去检查微任务队列和宏任务队列,然后取出任务一,放入执行栈,任务一执行后输出setTimeout2,将内部的setTimeout放入宏任务队列队尾 5.输出setTimeout2后,会重复上述检查微任务宏任务队列的步骤,输出setTimeout1-1,然后在次检查,输出setTimeout2-1
例2:
console.log(1); new Promise((resolve, reject)=>{ console.log('promise'); resolve(); }).then((data)=>{ console.log('then1'); }) setTimeout(function(){ console.log('setTimeout1') setTimeout(function(){ console.log('setTimeout1-1'); }) new Promise((resolve, reject)=>{ console.log('inner-promise'); resolve(); }).then((data)=>{ console.log('inner-then'); }) },0) setTimeout(function(){ console.log('setTimeout2'); setTimeout(function(){ console.log('setTimeout2-1'); }) },0) console.log(2)执行结果:
执行过程说明:
- 首先明确一点,promise中的代码都是同步执行的,promise.then才是异步的,且是微任务。因此首先输出的是1、promise、2,输出这些后任务队列中的情况如下图:

- 执行1之后,执行栈被清空,首先检查微任务队列,发下有任务,则取出放入主线程执行栈执行,输出then1,主线程任务栈清空。
- 再次检查微任务队列,没有发现排队中的微任务,开始检查宏任务队列,取出任务1,输出setTimeout1,然后将setTimeout放入宏任务队列,同步执行promise,输出inner-promise,将then放入微任务队列,主线程任务栈清空。此时任务队列的情况如下:

- 再次开始检查微任务队列,发现步骤3中放入的微任务,取出执行,输出inner-then,执行栈被清空
- 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务2,放入执行栈执行,输出setTimeout2,将任务2中的setTimeout放入宏任务队列(编号任务4),清空执行栈
- 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务3,输出setTimeout1-1,清空执行栈
- 再次开始检查微任务队列,没有可执行的任务,于是检查宏任务队列,取出任务4,输出setTimeout2-1,清空执行栈
- 再次开始检查微任务队列和微任务队列,发现没有可执行的任务,程序执行完毕。
小结
浏览器的事件环机制其实就是JS在执行过程中,安排各个任务如何排好队的机制。是主线程执行栈和微任务队列、宏任务队列之间的一个循环调度的机制。
我们需要注意的是主线程每次从宏任务队列里取任务,是一个一个的取的,而从微任务队列取任务时,是全部的取出。这也是浏览器Event loop和node的Event loop区别中的一点。