事件环

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

  Promise.resolve().then(() => {
    console.log('p1')
  })
  Promise.resolve().then(() => {
    console.log('p2')
  })
})

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

  Promise.resolve().then(() => {
    console.log('p3')
  })
  Promise.resolve().then(() => {
    console.log('p4')
  })
})
js

浏览器中的事件环

事件环执行顺序

  • 从上至下执行所有的同步代码
  • 执行过程中将遇到的宏任务与微任务添加至相应的队列
  • 同步代码执行完毕后,执行满足条件的微任务回调
    • 每执行一个宏任务,都会检查当前批次是否有需要执行的微任务队列
  • 微任务执行完毕后执行所有满足需求的宏任务回调
  • 循环事件环操作

Node.js 中的事件环

组成部分

timers 

pending callbacks

idle, prepare

poll

check

close callbacks
  • timers: 执行 setTimeout 与 setInterval 回调
  • pending callbacks:执行操作系统的回调,例如 tcp udp
  • idle,prepare:只在系统内部进行使用
  • poll:执行与 I/O 相关回调
  • check:执行 setImmediate 中的回调
  • close callbacks:执行 close 事件的回调

执行顺序

  • 执行同步代码,将不同的任务添加至相应的队列
  • 所有同步代码执行后会执行满足条件的微任务
  • 所有微任务代码执行后会执行 timer 队列中满足的宏任务
  • timer 中的所有宏任务执行完成后就会依次切换队列
    • 完成队列切换之前会先清空微任务代码

我们仅需要关心 timers、poll、check 队列。

代码分析

setTimeout(() => {
  console.log('s1')
})

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

console.log('start')

process.nextTick(() => {
  console.log('tick')
})

setImmediate(() => {
  console.log('setImmediate')
})

console.log('end')

// start
// end
// tick
// p1
// s1
// setImmediate
js

对于微任务来说,nextTick 的优先级要高于 Promise。

start end tick p1

s1 - timers,null - poll,setImmeriate - check

执行步骤梳理

通过一段代码分析事件循环执行步骤。

setTimeout(() => {
  console.log('s1')
  Promise.resolve().then(() => {
    console.log('p1')
  })
  process.nextTick(() => {
    console.log('t1')
  })
})

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

console.log('start')

setTimeout(() => {
  console.log('s2')
  Promise.resolve().then(() => {
    console.log('p3')
  })
  process.nextTick(() => {
    console.log('t2')
  })
})

console.log('end')

// start
// end
// p2
// s1
// t1
// p1
// s2
// t2
// p3
js

Node 与浏览器事件环对比

  • 任务队列数不同
    • 浏览器只有两个任务队列
    • Node.js 中有 6 个事件队列
  • 微任务执行时机
    • 二者都会在同步代码执行完毕后执行微任务
  • 微任务优先级不同
    • 浏览器事件环中,微任务存放于事件队列,先进先出
    • Node.js 中 process.nextTick 先于 promise.then

Node.js 常见问题

setTimeout(() => {
  console.log('timeout')
})

setImmediate(() => {
  console.log('immediate')
})
js

Node.js 中执行上述代码执行结果并不是唯一的,可能会先输出 timeout,也可能先输出 immediate。

const fs = require('fs')

fs.readFile('./test.txt', () => {
  setTimeout(() => {
    console.log('timeout')
  })

  setImmediate(() => {
    console.log('immediate')
  })
})
js

如果将上述代码包裹在一个 IO 操作中,执行顺序就固定了,结果永远是先输出 immediate,然后再输出 timeout。

这里会优先执行 poll 队列,然后再执行 check 队列,最后才能执行到 timers 事件队列。

默认情况下 setTimeout 与 setImmediate 执行顺序是随机的,因为 setTimeout 后面的延时时间是不固定的。如果将它们放到 I/O 回调中,它们的执行顺序就会变成固定的,永远都是先输出 immediate 然后再输出 timeout。