Event Loop 事件循环机制

Event Loop 事件循环机制

浏览器

1. micro task 微任务

nextTick、callback、Promise、process.nextTick (node)、Object.observer (废除)、MutationObserver (H5)

2. macro task 宏任务

setTimeout、setInterval、I/O、script代码块、UI rendering、setImmediate (node)

事件循环的执行过程

同步代码-微任务-宏任务

  • 一个事件循环里面有很多个不同的任务队列,每个任务队列都是按照先进先出的顺序执行的。
  • 首先:一段代码就是一个宏任务,所以执行代码的时候,就是程序进入了主线程,主线程会根据不用的代码分为宏任务、微任务。
  • 第一次循环:从上到下,遇到同步代码执行,遇到宏任务/微任务放到对应的队列
  • 事件队列在同步任务执行完成之后,首先会执行nextTick,等nextTick执行完,会先执行微任务,等到微任务队列空了之后,才会去执行宏任务,若中间添加了微任务到到微任务队列,会继续去执行微任务队列,然后再回到宏任务队列,JS引擎存在monitoring进程,会不断监听任务队列
  • 事件队列严格按照事件先后顺序将任务压入任务栈执行,当执行栈空时,浏览器会不断检查事件队列,若不为空,在每一个任务结束之后,浏览器你会对页面进行渲染
  • 注意:当从宏任务里面取宏任务时,每执行完一个宏任务都要检查微任务队列是否为空,如果不是空的则会一次性执行完所有的微任务,然后再进入下一个宏任务

特殊

  • 优先级
  1. nextTick > Promise
  2. Process.nextTick > Promise
  3. SetTimeout > setTmmediate
  • 不确定
  1. setTimeout执行一次,setInterval循环执行。其中setInterval执行的顺序不确定,次数也不确定。
  2. setTimeout、setTmmediate顺序不确定
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
console.log('start')
Promise.resolve()
.then(() => {
setTimeout(() => {
console.log('out0')
}, 0)
console.log('promise 1')
})
.then(() => {
console.log('promise 2')
setTimeout(() => {
console.log('out1')
}, 0)
})

setTimeout(() => {
console.log('out2')
}, 0)
Promise.resolve()
.then(() => {
console.log('promise 3')
})
.then(() => {
console.log('promise 4')
setTimeout(() => {
console.log('out3')
}, 0)
})
console.log('end');



// start end promise1 promise3 promise2 promise4 out2 out0 out1 out3

async

1. async做了一件什么事情?

async将你的函数返回值(自动地)转换为promise对象,不需要显式地返回promise对象

2. await的作用

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
async function async1() {
console.log('async1')
await async2()
console.log('async1end')
}

async function async2() {
console.log('async2')
}

console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)

async1();

new Promise( function( resolve ) {
console.log('promise1')
resolve();
} ).then( function() {
console.log('promise2')
} )

console.log('end')

// start async1 async2 promise1 end promise2 async1end setTimeout

js主线程走到并输出“script start”,由于setTimeout执行的优先级低于主线程上的任务和Promise上的任务所以要等到最后,直到执行
async1()函数内,输出“async1 start”,在执行async2()函数输出“async2”,由于走到async2()函数时遇到await,要跳出async1()函
数体外,接着代码往下跑,执行new Promise对象,输出“promise1”,new Promise对象进入到Promise队列中,继续往下走,输
出“script end”,现在回到async1()函数体内中的await async2()处,由于await后面接着async2()返回的是Promise对象还要再次跳出
async1()函数体外继续执行以外的代码,这时候正好有Promise队列中的then需要执行,于是输出“promise2”,剩下没代码可以跑了,
setTimeout仍然靠后优先级低,再次回到async1()函数体内,接着执行输出“async1 end”,最后剩下主线程和Promise队列的任务都执行
完成了,就输出“setTimeout”

  • await等待的是右侧的[表达式结果],如果右侧是一个函数,等待的是右侧函数的返回值,如果右侧的表达式则是右侧的表达式
  • await在等待时会让出线程阻塞后面的执行。await的执行顺序为从右到左,会阻塞后面的代码执行,但并不是直接阻塞await的表达式。
  • await之后如果不是promise,await会阻塞后面的代码,会先执行async外面的同步代码,等外面的同步代码执行完成在执行async中的代码。
  • 如果它等到的是一个 promise 对象,await 也会暂停async后面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。

nodeJS

  • nodejs开始的时候会初始化事件循环,处理脚本,脚本可能会进行异步API调用,定时任务或者process.nextTick(),然后开始事件循环
  • 在每个阶段完成后,而不是每一个宏任务完成后,微任务队列就会被执行

六个阶段

  1. timers(计时器):执行满足条件的setTimout()和setInterval()的回调
  2. I/O回调:上一轮循环中的少数I/O callback被延迟到这一阶段执行(系统调用错误,、网络通讯错误回调)
  3. idle prepare
  4. poll(轮询):获取新的I/O事件,nodejs会话当进行阻塞
  5. check:执行setTmmediate的回调
  6. close callback:执行close事件的callback

timer

timers 阶段会执行 setTimeout 和 setInterval

一个 timer 指定的时间并不是准确时间,而是在达到这个时间后尽快执行回调,可能会因为系统正在执行别的事务而延迟。

下限的时间有一个范围:[1, 2147483647] ,如果设定的时间不在这个范围,将被设置为 1。

I/O

I/O 阶段会执行除了 close 事件,定时器和 setImmediate 的回调

idle, prepare

idle, prepare 阶段内部实现

poll

poll 阶段很重要,这一阶段中,系统会做两件事情

执行到点的定时器
执行 poll 队列中的事件
并且当 poll 中没有定时器的情况下,会发现以下两件事情

如果 poll 队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制
如果 poll 队列为空,会有两件事发生
如果有 setImmediate 需要执行,poll 阶段会停止并且进入到 check 阶段执行 setImmediate
如果没有 setImmediate 需要执行,会等待回调被加入到队列中并立即执行回调
如果有别的定时器需要被执行,会回到 timer 阶段执行回调。

check

check 阶段执行 setImmediate

close callbacks

close callbacks 阶段执行 close 事件

并且在 Node 中,有些情况下的定时器执行顺序是随机的

1
2
3
4
5
6
7
8
9
10
setTimeout(() => {
console.log('setTimeout')
}, 0)
setImmediate(() => {
console.log('setImmediate')
})
// 这里可能会输出 setTimeout,setImmediate
// 可能也会相反的输出,这取决于性能
// 因为可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate
// 否则会执行 setTimeout

当然在这种情况下,执行顺序是相同的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
var fs = require('fs')

fs.readFile(__filename, () => {
setTimeout(() => {
console.log('timeout')
}, 0)
setImmediate(() => {
console.log('immediate')
})
})
// 因为 readFile 的回调在 poll 中执行
// 发现有 setImmediate ,所以会立即跳到 check 阶段执行回调
// 再去 timer 阶段执行 setTimeout
// 所以以上输出一定是 setImmediate,setTimeout

上面介绍的都是 macrotask 的执行情况,microtask 会在以上每个阶段完成后立即执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
setTimeout(() => {
console.log('timer1')

Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)

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

Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)

// 以上代码在浏览器和 node 中打印情况是不同的
// 浏览器中一定打印 timer1, promise1, timer2, promise2
// node 中可能打印 timer1, timer2, promise1, promise2
// 也可能打印 timer1, promise1, timer2, promise2

Node 中的 process.nextTick 会先于其他 microtask 执行。

1
2
3
4
5
6
7
8
9
10
11
12
setTimeout(() => {
console.log('timer1')

Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)

process.nextTick(() => {
console.log('nextTick')
})
// nextTick, timer1, promise1