浏览器线程-事件循环(Event Loop)

1,833 阅读5分钟

事由起因,咋们先来了解下浏览器的进程与线程

  • 首先,浏览器是多进程,如:
    • 浏览器主进程
    • 第三方插件进程
    • GPU进程
    • tab页进程
  • 一个进程中,可以存在一个或多个线程

image.png

浏览器进程

  • 在浏览器中每开启一个tab页,就开启了对应tab页进程,每个页面互不影响
  • 一个页面相当于一个进程,一个进程有多个线程互相配合

浏览器tab页中的进程主要由以下线程构成

主线程(JS引擎线程)

  1. 浏览器存在很多线程(事件触发线程互相操作)

GUI渲染线程(常驻线程)

  1. 渲染浏览器界面,解析html,css,构建dom树,布局和绘制
  2. 该线程与JS线程互斥,当js引擎执行时,该引擎会被挂起,会保存在一个队列中等到JS引擎空闲时立即执行

JS引擎线程(常驻线程)(JS内核)

  1. 负责处理JavaScript脚本程序(例如V8引擎),解析JavaScript脚本,运行代码
  2. 一个页面中只有一个JS线程在运行JS程序
  3. 如果JS引擎运行时间过长,会造成阻塞页面加载,影响GUI渲染线程页面加载
  4. 当JS引擎修改DOM的时候,如果同时运行GUI引擎,会造成页面数据不一致
  5. 因此GUI更新会放到任务队列中,等JS引擎空闲时立即执行

浏览器事件线程(onclick)(常驻线程)

  1. 该事件归属于浏览器,用来控制事件循环
  2. JS引擎遇到事件时(不是首先同步处理的事件),DomEvent事件等,会将这些任务添加到该事件线程中
  3. 当事件符合触发条件被触发时(如点击,移动等操作),该线程会把事件添加到任务队列末尾,等待JS引擎的处理
  4. 由于JS时单线程,所以任务队列需要等待JS引擎依次处理
  5. 定时器触发线程(setTimeout和setInterval)
  6. 定时器不是由JS引擎计数的,因为JS是单线程,如果处于阻塞状态就会影响计时的准确
  7. 所以当计时完毕后,会添加到任务队列末尾,等待JS引擎执行
  8. 注意!!!
    1. 如果JS引擎处理其他任务超过定时器时间
    2. 如定时器5000ms,比如循环事件用了6000ms,就算定时器线程在5000ms的时候把执行函数放入到任务事件队列末尾,也必须等循环结束后才能调用,这就造成实际是6000ms的时候才触发定时器

异步http请求线程

  1. 当XMLHttpRequest连接后,浏览器会新开一个线程请求
  2. 当检测到状态变更时(onreadystateChange = function () {})
  3. 如果设有回调函数,该异步线程就产生状态变更事件,将回调函数放入事件队列末尾,再由JS引擎执行

事件循环(Event Loop)

Event loop事件轮询处理

  • 先来了解下,js引擎是怎样的运行方式

栈

  1. 后进先出,先进后出
  2. 数据存储只能从顶部逐个存入,取出时也需从顶部逐个取出

image.png

堆

  1. 无序的key-value(键值对)存储方式

队列

  1. 先进先出
  2. 数据存储从队尾插入,从队头取出

事件执行

  1. JS引擎先处理同步代码(宏任务),微任务和定时器等异步代码放入对应线程中等待触发
  2. 当微任务和异步代码符合触发条件,将会把回调函数放入任务队列末尾等待JS引擎执行
  3. 当用户操作event事件触发时,事件触发线程会将对应事件函数放入任务队列末尾

任务优先级

graph LR
id1(队列任务优先级) ==> id2(宏任务) == 同步任务 -->id3(微任务) == 异步任务--> id4(异步定时器任务)
style id1 fill:#ccf,stroke-width:4px;
style id2 fill:#ccf,stroke-width:4px;
style id3 fill:#ccf,stroke-width:4px;
style id4 fill:#ccf,stroke-width:4px;

任务执行线路

graph TB
开始执行队列任务...
--> 宏任务== 微任务 ==>
GUI渲染引擎线程 == ......
--> 宏任务2 == 微任务 ==>
GUI渲染引擎线程2
-- Event Loop --> 开始执行队列任务...

宏任务(同步任务)和微任务(异步任务)

graph TB
A["执行队列任务"]
      A== 进入执行栈 -->B[同步 or 异步任务]
      B== 同步 -->C(立即执行) == 同步任务执行完成 --> 完成后出栈 == 执行异步任务 --> 继续执行队列任务;
      B== 异步 -->D[定时器触发线程] == 注册对应的回调函数 --> http触发线程 == 异步函数放入队列末尾 --> 继续执行队列任务 == 执行完毕 --> id>循环执行队列任务] --> A

image.png

let oneTask = () => {
    console.log('第一个宏任务');
    queueMicrotask(() => console.log('第一个微任务'));
}
oneTask()
setTimeout(() => console.log('处在中间位置的定时器任务'));
Promise.resolve('promise任务').then((result) => console.log(result));
let twoTask = () => {
    console.log('第二个宏任务');
    queueMicrotask(() => console.log('第二个微任务'));
}
twoTask()

image.png

graph LR
宏任务 --> 微任务 --> 异步定时器任务

再次证明宏任务(同步任务)往往会优先放入事件队列中执行

  1. 普通函数:属于宏任务
  2. queueMicrotask:创建一个微任务
  3. promise:也属于微任务
  4. setTimeout:属于异步任务

异步任务

  • 在这里,再介绍两个异步任务 window.requestAnimationFrame
  1. 此Api作用为请求动画帧
  2. 实现动画可以用Css3,canvas,此Api专门用于请求动画
    • 屏幕刷新频率:屏幕每秒出现图像的次数。普通笔记本为60Hz
    • 动画原理:计算机每1Hz用时16.7ms,由于人眼的视觉停留,所以看起来是流畅的移动
    • setTimeout:由于是异步定时任务
      1. 只有当主线程任务执行完后才会执行队列中的任务,因此实际执行时间总是比设定时间要晚
      2. 异步定时任务固定时间间隔不一定与屏幕刷新时间相同,会引起丢帧
    • requestAnimationFrame:只会根据计算机刷新频率执行回调任务,会定时render界面
let progress = 0;
//回调函数
console.time()
const render = () => {
 progress += 1; //修改图像的位置
 if (progress < 100) {
 //在动画没有结束前,递归渲染
     window.requestAnimationFrame(render);
 } else {
     console.timeEnd()
 }
}
//第一帧渲染
window.requestAnimationFrame(render)

window.requestIdleCallback

  1. 此Api作用与浏览器空闲时段内调用的函数排队
  2. 优先级低于window.requestAnimationFrame
  3. 在空闲回调函数中调用requestIdleCallback(),以便在下一次通过事件循环之前调度另一个回调。
window.requestIdleCallback(() => console.log('空闲调用该函数'));
graph LR
宏任务 --> 微任务 --> requestAnimationFrame --> requestIdleCallback --> 异步定时任务

总结

  1. 一个tab浏览器页面可以理解为拥有一个宏任务队列、一个微任务队列
  2. 在script加载时,先执行同步代码(宏任务),当执行栈清空后,将从微任务队列中依次取出放入到执行栈执行
  3. 当执行栈执行完微任务后,等待下一个宏任务或微任务进入执行栈,继续执行任务,以此构成事件循环。