2 个名词

  • microtask: 称之为小型任务
  • macrotask(task queue): 称之为大型任务

事件循环模型

当执行栈(call stack)为空时,一个事件循环会按照下面的步骤进行:


  1. 从macrotask队列中选择最老一个任务taskA

  2. 如果taskA不存在,即macrotask队列是空的,那么进行步骤6

  3. 设置当前运行的任务为taskA

  4. 运行taskA(即执行taskA它的回调函数)

  5. 设置当前运行的任务为空,并从macrotask中移除taskA

  6. 开始处理microtask

    • (a)从microtask中选择最老的任务taskX

    • (b)如果taskX不存在,即microtask是空的,那么跳到步骤(g)

    • (c)设置当前运行的任务为taskX

    • (d)运行taskX(即执行taskX的回调函数)

    • (e)设置当前运行的任务为空,并从microtask中移除taskX

    • (f)从microtask选择下一个最老的任务,步骤(b)

    • (g)重复以上步骤,直到microtask中的任务全部执行完毕



  7. 继续回到步骤1

精简的概括就是:

  • 先从 macrotask 队列中选择最老的一个任务开始执行,然后移除这个最老的任务
  • 再执行 microtask 队列中所有的任务,然后移除他们
  • 继续下一轮,重复以上 2 步

需要知道的一些事情:


  1. 当一个macrotask任务正在运行的时候,新的事件可以被注册,也即创建新的任务。例如:

    • promiseA.then()的回调是一个任务

      • promiseA如果处于resolve/reject状态时,那么该任务被推到事件循环的当前轮次的microtask中

      • promiseA处于pending状态时,那么该任务将会被推到事件循环的下一个轮次的microtask中。



    • setTimeout(callback, n)的回调是一个任务,将会被推到macrotask中,及时n=0。



  2. 如果事件循环正在执行microtask中的任务,那么你可以继续往microtask中添加任务,这些任务都会在本轮次的循环中执行。

  3. 只有等到本轮次的microtask中的所有任务执行完毕,才会执行下一轮的macrotask任务。

  4. 常见的microtask任务:process.nextTick,Promise, Object.observe, MutationObserver

  5. 常见的macrotask任务:setTimeout,setInterval, setImmediate,Dom事件(click, scroll, mouseup等), ajax,I/O, UI rendering等。还需要注意的是,整个的脚本也是一个macrotask任务。

经过上面的分析,可以看如下例子输出:

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
console.log('script start')

const interval = setInterval(() => {
console.log('setInterval')
}, 0)

setTimeout(() => {
console.log('setTimeout1')

Promise.resolve()
.then(() => {
console.log('promise3')
})
.then(() => {
console.log('promise4')
})
.then(() => {
setTimeout(() => {
console.log('setTimeout2')

Promise.resolve()
.then(() => {
console.log('promise5')
})
.then(() => {
console.log('promise6')
})
.then(() => {
clearInterval(interval)
})
})
})
})

Promise.resolve()
.then(() => {
console.log('promise1')
})
.then(() => {
console.log('promise2')
})

// script start
// promise1
// promise2
// setInterval
// setTimeout1
// promise3
// promise4
// interval
// setTimout2
// promise5
// pormise6

call stack(执行栈)与 event loop(事件循环)之间的关系

为了形象的表示,可以参看下图:

关于他们之间的关系的说明,此处就不在叙述了,具体可以参看这里

参考文章如下:
microtask and macrotask
event loop
task-queue spec
call stack