事件循环

简介

事件循环在浏览器和node环境机制不同, 一个是h5规范的实现, 一个集成libuv库的功能. 理解事件循环对js异步非阻塞的理解有会有所帮助.
在浏览器里每个web worker都有自己的事件循环系统.

概念

  • js只有一个线程:避免DOM渲染冲突.
    • 浏览器需要渲染DOM
    • JS可以修改DOM结构
    • JS执行的时候,浏览器DOM渲染会暂停
    • 两端JS也不能同时执行(都修改DOM就冲突了)
    • webworker支持多线程,但是不能访问DOM
  • js运行时包含call stack(回调要执行了就放入栈里), heap, task queue.
  • 事件队列,分为
    • micro taskqueue: Promise的then, MutationObserver, process.nextTick
    • macro taskqueue: setInterval, setTimeout, setImmediate(ie), MessageChannel, dispatching event(js执行node.click()方法不算)

浏览器环境

执行过程:

  1. 执行完主执行线程中的任务。
  2. 取出Microtask Queue中任务执行直到清空。
  3. 取出Macrotask Queue中一个任务执行。
  4. 取出Microtask Queue中任务执行直到清空。
  5. 重复3和4。

    note

    • HTML中每一个正在被解析script标签中的代码是一个独立的取出Macrotask. 即会执行完前面的script中创建的microtask再执行后面的script中的同步代码
    • promise的回调then和catch才是microtask,本身的内部代码不是。
    • microtask执行过程中产生的microtask会添加队尾然后一并执行.
    • 同步代码执行时遇到异步代码, 会加入到任务队列中. 同步执行完后, 刷一遍microtask queue. 执行一个macro task, 再刷一遍microtask queue. 如此反复.
    • pseudo code
      1
      2
      3
      4
      while (true) {
      宏任务队列.shift()()
      微任务队列全部任务()
      }

demo

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
<script>
console.log('script1');
Promise.resolve('script1-mi0').then(a => console.log(a))
setTimeout(() => {
console.log('script1-ma1')
Promise.resolve().then(data => console.log('script1-mi1'))
Promise.resolve().then(data => console.log('script1-mi2'))
}, 0)
setTimeout(() => {
console.log('script1-ma2')
Promise.resolve().then(data => console.log('script1-mi3'))
Promise.resolve().then(data => console.log('script1-mi4'))
}, 0)
</script>
<script>
console.log('script2');
Promise.resolve('script2-mi0').then(a => console.log(a))
setTimeout(() => {
console.log('script2-ma1')
Promise.resolve().then(data => console.log('script2-mi1'))
Promise.resolve().then(data => console.log('script2-mi2'))
}, 0)
setTimeout(() => {
console.log('script2-ma2')
Promise.resolve().then(data => console.log('script2-mi3'))
Promise.resolve().then(data => console.log('script2-mi4'))
}, 0)
</script>

output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
script1 执行完主执行线程中的任务。
script1-mi0 取出Microtask Queue中任务执行直到清空。
script2 // 等等前面script的microtask
script2-mi0
script1-ma1 取出Macrotask Queue中一个任务执行。
script1-mi1
script1-mi2 取出Microtask Queue中任务执行直到清空。
script1-ma2 取出Macrotask Queue中一个任务执行。
script1-mi3
script1-mi4 取出Microtask Queue中任务执行直到清空。
script2-ma1 script2的macro task后添加后执行
script2-mi1
script2-mi2
script2-ma2
script2-mi3
script2-mi4

node环境

事件循环6个阶段执行顺序:

  • When the queue has been exhausted or the callback limit is reached, the event loop will move to the next phase, and so on.
    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
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    ┌───────────────────────────┐
    ┌─>│ timers │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ pending callbacks │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    │ │ idle, prepare │
    │ └─────────────┬─────────────┘ ┌───────────────┐
    │ ┌─────────────┴─────────────┐ │ incoming: │
    │ │ poll │<─────┤ connections, │
    │ └─────────────┬─────────────┘ │ data, etc. │
    │ ┌─────────────┴─────────────┐ └───────────────┘
    │ │ check │
    │ └─────────────┬─────────────┘
    │ ┌─────────────┴─────────────┐
    └──┤ close callbacks │
    └───────────────────────────┘
    ```
    1. timers:执行setTimeout() 和 setInterval()中到期的callback。
    1. I/O callbacks:上一轮循环中有少数的I/Ocallback会被延迟到这一轮的这一阶段执行
    1. idle, prepare:仅内部使用
    1. poll:最为重要的阶段,执行I/O callback,在适当的条件下会阻塞在这个阶段
    1. check:执行setImmediate的callback
    1. close callbacks:执行close事件的callback,例如socket.on("close",func)
    ### 循环之前
    在进入第一次循环之前,会先进行如下操作:
    - 同步任务
    - 发出异步请求
    - 规划定时器生效的时间
    - 执行process.nextTick()
    ### 开始循环
    按照我们的循环的6个阶段依次执行,每次拿出当前阶段中的全部任务执行,清空nextTick Queue,清空Microtask Queue。再执行下一阶段,全部6个阶段执行完毕后,进入下轮循环。即:
    - 清空当前循环内的Timers Queue,清空NextTick Queue,清空Microtask Queue。
    - 清空当前循环内的I/O Queue,清空NextTick Queue,清空Microtask Queue。
    - 清空当前循环内的Check Queu,清空NextTick Queue,清空Microtask Queue。
    - 清空当前循环内的Close Queu,清空NextTick Queue,清空Microtask Queue。
    进入下轮循环。
    可以看出,nextTick优先级比promise等microtask高。setTimeout和setInterval优先级比setImmediate高。
    ### 注意
    - 如果在timers阶段执行时创建了setImmediate则会在此轮循环的check阶段执行,如果在timers阶段创建了setTimeout,由于timers已取出完毕,则会进入下轮循环,check阶段创建timers任务同理。
    - setTimeout优先级比setImmediate高,但是由于setTimeout(fn,0)的真正延迟不可能完全为0秒,可能出现先创建的setTimeout(fn,0)而比setImmediate的回调后执行的情况。
    - 伪代码
    ```js
    while (true) {
    loop.forEach((阶段) => {
    阶段全部任务()
    nextTick全部任务()
    microTask全部任务()
    })
    loop = loop.next
    }

demo

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
function sleep(time) {
let startTime = new Date()
while (new Date() - startTime < time) {}
console.log('1s over')
}
setTimeout(() => {
console.log('setTimeout - 1')
setTimeout(() => {
console.log('setTimeout - 1 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 1 - then - then')
})
})
sleep(1000)
})
setTimeout(() => {
console.log('setTimeout - 2')
setTimeout(() => {
console.log('setTimeout - 2 - 1')
sleep(1000)
})
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then')
new Promise(resolve => resolve()).then(() => {
console.log('setTimeout - 2 - then - then')
})
})
sleep(1000)
})

node执行后结果

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout - 1
1s over
setTimeout - 2 //1、2为单阶段task
1s over
setTimeout - 1 - then
setTimeout - 2 - then
setTimeout - 1 - then - then
setTimeout - 2 - then - then
setTimeout - 1 - 1
1s over
setTimeout - 2 - 1
1s over

参考