JS 事件循环机制和实战

140 阅读3分钟

JS的event loop是常考常用的知识点

前置知识

JS虽然是单线程的,但是浏览器(宿主环境)是多线程的:

  • JS主线程:与GUI线程互斥
  • GUI线程:界面渲染和绘制、解析HTML、CSS、构建DOM树和render树
  • 定时器线程
  • http请求线程
  • 等等

在浏览器中不同的异步操作由不同的的浏览器内核调度执行,比如setTimeout由定时器线程处理,界面的渲染由GUI线程处理,不同的异步操作添加到任务队列的时机不同。

JS的事件循环机制

  • 首先,同步任务进入主线程,异步任务先进入event table
  • 异步任务时机成熟后会进入event queue事件队列
  • 主线程执行完成后会去查看事件队列是否有任务在排队,有就推入主线程

以上过程往复循环就是event loop

异步任务还有细分

分类

异步任务还可以分为宏任务微任务,在js中两者执行的优先级不同,微任务为js引擎处理,宏任务由宿主环境处理

常见的宏任务:

  • setTimeout setInterval
  • setImmediate
  • script代码块

常见的微任务:

  • Promise.then() catch()
  • Async/await
  • I/O

过程

js的同步任务会形成一个执行栈,也是一个队列,宏任务会进入宏任务队列,微任务进入微任务队列,执行栈清空后,会先去看微任务队列,将微任务依次加到执行栈末尾,直到微任务任务全部清空,然后再去检查宏任务队列,并将宏任务加入执行栈,最终执行栈完全清空

好几个注意点提前说:

  1. setTimeout | setInterval优先级高于setImmediate
  2. 创建Promise对象时,Promise构造函数中的语句是同步执行,then/catch的回调是异步的
  3. 就算setTimeout时间间隔设置为0也是宏任务
  4. 执行栈清空后,会先push一个微任务进入执行栈中,此时如果产生了新的微任务,继续推入微任务队列,执行栈清空后,继续push微任务到执行栈,直到微任务队列清空

实战演练

console.log("1")

setTimeout(()=>{
  console.log("2")
  Promise.resolve().then(()=>{
    console.log("3")
  })
},0)

setTimeout(()=>{
  console.log("4")
  Promise.resolve().then(()=>{
    console.log("5")
  })
},0)

const p = new Promise((resolve,reject)=>{
  console.log("6")
  resolve("7")
})
p.then((data)=>{
  console.log(data)
})
console.log("8")

js从上往下执行一遍会形成如下图所示的分配情况: image.png

  1. 按照event loop机制首先会清空执行栈,打印 1 6 8

  2. 然后检查微任务队列,推入执行栈执行,打印 7

  3. 微任务清空后,检查宏任务队列,此时堆栈状态更新为下图所示,开启新的循环

image.png
  1. 执行栈中有内容,先打印 2,执行栈清空
  2. 然后检查微任务,推入执行栈,打印 3 微任务队列清空
  3. 另一个setTimeout也是同样
  4. 最终打印顺序为1 6 8 7 2 3 4 5