Event Loop事件循环

1,388 阅读6分钟

CPU、进程、线程之间的关系

  • cpu 是计算机的核心,承担了所有计算机的任务

  • 进程 cpu资源分配的最小单位

  • 线程 cpu调度的最小单位

三者之间的的关系在之前看到的一篇文章里说就好比一个工厂,这个工厂就是cpu

工厂每次只能提供一个车间的电力,也就是当一个车间开工的时候,其它所有车间不能工作。

这就意味着一个cpu一次只能运行一个任务,而进程就是这一个个车间。任何时刻,cpu都只能进行一个进程,其他进程就进入了非运行状态。

每个车间里面有很多的工人,这些工人共享一个车间。

这就相当于一个进程可以存在多个线程,多个线程共享进程的资源。

浏览器是多进程的

浏览器是多进程的,大家可以打开任务管理器,在进程中看看Chrome浏览器(或任意浏览器打开多个tab页面):

可以看出Chrome浏览器中有多个进程。

浏览器进程分类

  • Browser进程
    • 浏览器的主进程,负责协调控制其它子进程
  • 第三方插件进程
    • 每使用一个插件,相当于一个进程,只有在使用该插件的时候创建
  • GPU进程
    • 最多一个,用于3D绘制
  • 浏览器渲染进程(内核)
    • 默认每个Tab页面一个进程,互不影响
    • 控制页面渲染,脚本执行,事件处理等

浏览器渲染进程(内核)

渲染进程当然存在多个线程啦,让我们来看看吧:

  • GUI线程
    • 负责渲染浏览器界面,解析HTML、CSS。
    • 当页面重绘或者引发回流的时候,该线程会执行
    • 该线程与JS引擎线程是互斥的,二者不可以同时进行
  • JS引擎线程
    • 负责js脚本程序
    • 只有一个js引擎线程
    • 同样,与GUI线程互斥
  • 事件触发线程
    • 用来控制事件循环
    • 当js引擎执行代码时,会将对应任务添加到事件触发线程
    • 当对应事件触发,线程会把事件添加到任务队列队尾,等待响应
  • 定时触发线程
    • setInterval()和setTimeout()所在的线程
    • 定时任务不是由js引擎计数的
  • 异步http请求线程
    • 浏览器有一个单独的线程用于处理ajax请求

GUI渲染线程和JS引擎线程互斥: 因为js时可以操作DOM的,如果在修改这些元素属性的时候同时渲染页面,那渲染前后获得的元素数据可能会不一致。所以为防止渲染出现不可预期的结果,浏览器设置了二者是互斥的。

Event Loop

什么是Event loop

  • js分为同步任务和异步任务

  • 同步任务在js引擎线程上执行,形成一个执行栈

  • 事件触发线程管理一个任务队列

    • 定时触发线程本身是一个同步任务,但其中的回调函数是异步任务,所以js引擎线程会通知定时器触发线程,当定时器触发线程接收到消息后,会在等待的时间后将回调函数放入到任务队列中去
    • 同样,异步http请求线程会接收到一个来自js引擎线程的通知,发送一个网络请求,当异步http请求线程接收到消息后,在请求成功后,将回调函数放入任务队列中去
  • 执行栈中同步任务执行完毕,js引擎线程空闲,系统会自动识别任务队列,将可运行的异步任务添加到执行栈中。

用一张图来解释,可能更加清楚:

宏任务和微任务

微任务和宏任务皆为异步任务,它们都属于一个队列,主要区别在于他们的执行顺序,Event Loop的走向和取值。

什么是宏任务

我们将每次执行栈执行的代码当作一个宏任务,前文中提到过GUI渲染线程js引擎线程,它们二者是互斥的,浏览器为了使宏任务DOM任务有序进行,会在一个宏任务执行之后,下一个宏任务执行之前,对页面进行渲染。

宏任务有:

  • 整体代码script
  • setInterval()
  • setTImeout()
  • ...

什么是微任务

微任务可以理解为在宏任务执行后立马执行的任务,也就是说,当宏任务执行后,在渲染页面前,将所有的微任务执行完,再进行页面的渲染。

微任务有:

  • 原生Promise
  • process.nextTick
  • ...

如何区分宏任务与微任务

  • 宏任务是js宿主提供的

    • 目前较为常见宿主有浏览器node
  • 微任务是语言标准(js本身)提供的

    • javascript是由ECMA制定标准的,所以语言标准提供的就是微任务,如ES6中提供的Promise

综上所述,setTimeout是宿主提供的,Promise是ES6提供的,所以Promise会比setTimeout定时器更早执行。

栗子

1、

console.log(1)

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

console.log(3)  //输出结果为: 1、3、2
  • 因为Promise中的then方法异步回调函数,所以2最后执行。

2、

console.log(1)

setTimeout(() => {
  console.log(2)
}, 0);

new Promise((resolve, reject) => {
  console.log(3)
  resolve()
}).then(() => {
  console.log(4)
})

console.log(5) //输出结果: 1、3、5、4、2
  • 顺序执行,执行同步任务,可得出1

  • 遇到setTimeout,将其推入事件队列中,并且为宏任务

  • 遇到Promise,直接执行输出代码3,将then方法的回调函数放到微任务事件队列中

  • 执行同步任务,输出5

  • 第一轮实行完毕,查看微任务,即可输出4

  • 第二轮开始,先开始执行宏任务,输出2

3、

console.log(1)

new Promise((resolve, reject) => {
  console.log(2)
  setTimeout(() => {
    console.log(3)
  }, 0);
  resolve()
}).then(() => {
  console.log(4)
})

setTimeout(() => {
  console.log(5)
  new Promise((resolve, reject) => {
    console.log(6)
    resolve()
  }).then(() => {
    console.log(7)
  })
}, 0);

console.log(8) //输出结果: 1、2、8、4、3、5、6、7
  • 顺序执行,执行同步任务,输出1
  • 遇到Promise,直接输出2,将setTimeout(1)推入宏任务事件队列中,并将then方法回调函数放入微任务事件队列中
  • 遇到setTimeout(2),将其推入宏任务事件队列中去
  • 执行同步任务,输出8
  • 第一轮完成,找到微任务,输出4
  • 第二轮,先执行宏任务setTimeout(1)输出3
  • 查看没有微任务,即输出宏任务setTimeout(2)输出5
  • setTimeOut(2)中有Promise,直接执行输出6,并将then方法回调函数放入微任务
  • 执行微任务,输出7

总结

用一幅图来总结吧~

注:此文为本人学习过程中的笔记记录,如果有错误或者不准确的地方请大佬多多指教~