阅读 239

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

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

  • 首先,浏览器是多进程,如:
    • 浏览器主进程
    • 第三方插件进程
    • 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. 当执行栈执行完微任务后,等待下一个宏任务或微任务进入执行栈,继续执行任务,以此构成事件循环。
文章分类
前端
文章标签