概念

JavaScript是一门单线程语言,即同一时间只能执行一个任务,代码执行时是阻塞的。 因此浏览器提供了一些JS引擎不具备的功能: Web API,包含DOM APIsetTimeoutHTTP请求

在了解事件循环之前先了解俩个简单的概念

队列

队列是一种FIFO(First in, First out)数据结构,特点:先进先出

栈是一种LIFO(Last in, First out)数据结构,特点:后进先出

调用栈

调用栈 是js引擎中的一部分,本质上就是栈, 当函数被调用时,会进入到调用栈中。

  • 函数A执行,函数A入栈
  • 函数A中调用函数B,函数B入栈
  • 函数B执行完 出栈
  • 然后继续执行函数A,执行完函数A出栈
  • 栈空
function B() {
  console.log('B')
}
function A() {
  B()
  console.log('A')
}
A()
// 执行函数A -> 入栈,调用函数B -> 入栈,函数B执行完毕 -> 出栈,函数A执行完毕 -> 出栈。
1
2
3
4
5
6
7
8
9

上述都是同步代码,调用栈就能解释,但往往js中不仅仅是同步代码,还包含setTimeout网络请求异步任务

这时就需要引入 Event Table(事件表格)Event Queue(事件队列)

事件表格 & 事件队列

Event Table 可以理解成一张 事件->回调函数 对应表

它就是用来存储 JavaScript 中的异步事件 (request, setTimeout, IO等) 及其对应的回调函数的列表

Event Queue 简单理解就是 回调函数 队列,所以它也叫 Callback Queue

当 Event Table 中的事件被触发,事件对应的 回调函数 就会被 push 进这个 Event Queue,然后等待被执行

任务

在JavaScript中,任务被分为俩种:同步任务 & 异步任务

异步任务又包含 宏任务微任务

宏任务:

script整体代码、setIntervalsetTimeoutsetImmediate(浏览器暂不支持)、I/OUI Rendering

微任务:

Process.nextTick(node专有)、PromiseObject.observe(废弃)、MutationObserver(具体使用方式查看这里open in new window

Event Loop

先上流程图: 事件循环流程图

  1. 当代码开始进入script(宏任务),会从上往下逐行执行
  2. 同步任务直接在栈内等待被执行,异步任务会从Call Stack移入到Event Table注册
  3. Event Table中相对应的事件触发(或定时器时间到),Event Table会根据异步任务类型 分别将回调函数移动到 宏Event Queue| 微Event Queue排队等待
  4. Call Stack为空时,会从微Event Queue依次取出微任务到Call Stack执行,直至 微Event Queue被清空。
  • 注意 在执行微任务过程中,产生新的微任务,会加入到队列末尾,会在此周期内被调用执行。
  • 注意 当一个宏任务周期内所有微任务执行完毕之后,再执行下个宏任务之前,浏览器会自行判断执行UI Rendering。
  1. 宏Event Queue 取出宏任务放入Call Stack执行,重复 2-4步骤。

看下下面这道题哦, 执行环境 chrome控制台

console.log(1)

setTimeout(() => {
  console.log(6)
  setTimeout(() => {
    console.log(7)
  })
  console.log(8)
})

setTimeout(() => {
  console.log(2)
  Promise.resolve(3).then(console.log)
  Promise.reject(4).catch(console.log)
  console.log(5)
})

new Promise((resolve, reject) => {
  console.log(9)
  resolve(10)
  console.log(11)
}).then(res => {
  console.log(res)
  Promise.resolve(12).then(console.log)
})

new Promise((resolve, reject) => {
  console.log(13)
  setTimeout(() => {
    console.log(14)
  })
  console.log(15)
  reject(16)
}).catch(console.log)

console.log(17)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
答案

1 9 11 13 15 17 10 16 12 6 8 2 5 3 4 14 7

先分析

  1. 代码开始执行,整体代码属于script(宏任务),从上往下执行
  2. 同步任务会在Call Stack等待被执行,异步任务会放入到Event Table注册

此时 console.log(1) 、Promise函数体、 console.log(17)是同步任务 等待被执行

其它任务都作为异步任务移入到Event Table

Promise函数体中遇到的异步任务也是在同一周期内放入到Event Table

因此先输出 1 9 11 13 15 17

  1. 此时Call Stack执行栈同步任务执行完毕,先检查微任务队列,发现以下微任务 resolve(10), reject(16),依次执行,resolve(10)微任务中又遇到微任务resolve(12)放入到微任务队列最后,(要执行下个宏任务,必须清空微任务队列)

注意::此时Event Table中异步任务都基本被触发或时间到(如果有定时时间长的,可能此时还在EventTable

因此输出 10 16 12

  1. 微任务队列清空,Call Stack从宏任务队列取出队首的宏任务,重复Event Loop循环
setTimeout(() => {
  console.log(6)
  setTimeout(() => {
    console.log(7)
  })
  console.log(8)
})
1
2
3
4
5
6
7

执行同步任务 ,微任务放入微任务队列 宏任务 放入宏任务队列排队等待

先输出 6 8 然后检查微任务队列 为空 ,执行下个宏任务 输出 6 8

  1. 执行下个宏任务
setTimeout(() => {
  console.log(2)
  Promise.resolve(3).then(console.log)
  Promise.reject(4).catch(console.log)
  console.log(5)
})
1
2
3
4
5
6

执行同步任务 ,微任务放入微任务队列 宏任务 放入宏任务队列排队等待

先输出 2 5 然后检查微任务队列 ,清空微任务队列 输出 3 4 然后执行下个宏任务

  1. 执行下个宏任务 一直循环
上次更新: 2021/10/20 下午4:40:42
贡献者: 陈书进