Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

第274期 《从防抖节流优化到浏览器重绘更新动画再到浏览器内核多线程的复习》 by 花间 #270

Open
CuriosityLxn opened this issue Jun 21, 2020 · 1 comment
Labels

Comments

@CuriosityLxn
Copy link

起因

  • 在做A计划后台的时候,用到了键盘输入 keyword 防抖;
  • 修复支付宝小程序 bug 的时候看到杜松又一个 MR,修复点击事件节流掉帧的问题。

于是顺道复习了一下防抖和节流,看到了一个将动画更新频率和浏览器重绘频率(也是显示器刷新频率)一致化的方法—— window.requestAnimationFrame
这个方法没用过,就查了一下。

requestAnimationFrame 和计时器的区别:

这里有图,更直观

  • requestAnimationFrame 的 callback 在浏览器 UI 线程的队列中,由系统决定回调函数执行时机。跟随显示器刷新频率,精确;tab 页非激活状态不执行,节省 CPU 资源。

  • setTimeout 任务被定时器线程定时投放进事件队列中,JS 线程空闲后执行。只有当主线程上的任务执行完以后,才会去检查该队列的任务是否需要开始执行。会延时,会掉帧。

两种方法涉及到不同的浏览器内核的线程执行,于是顺道复习了下浏览器内核多进程多线程结构,知识点很多,先草草记录在这:

浏览器的多进程架构

Chrome浏览器使用多个进程来隔离不同的网页,打开一个 tab 页相当于起了一个进程。

为什么 Chrome 使用多进程架构

把所有网页都放进一个进程的浏览器面临在健壮性,响应速度,安全性方面的挑战。因为如果浏览器中的一个 tab 页崩了,将会导致其他被打开的网页应用。

为什么不使用多线程

同一进程下的线程共享内存空间和资源,线程之间有可能会恶意修改或者获取非授权数据等复杂的安全问题
进程间不共享内存,不会有太多安全问题。

浏览器内核

也被称为渲染引擎,通过获取页面内容(DOM)、整理信息(应用 CSS)、计算、组合,最终输出可视化图像结果。

Chrome 为每个 tab 启用单独的进程,所以每个 tab 都是其渲染引擎的实体。这些渲染引擎相互独立。

浏览器内核是多线程的

在内核控制下各线程相互配合以保持同步,一个浏览器通常由以下常驻线程组成:

  • GUI 渲染引擎线程
  • JavaScript 引擎线程
  • 定时触发器线程
  • 事件触发线程
  • 异步 HTTP 请求线程

GUI 渲染线程和JavaScript 引擎线程互斥!!!

GUI 渲染线程

负责渲染浏览器界面的 HTML 元素,重绘(Repaint)和回流(Reflow)在该线程执行。

JavaScript 引擎线程运行脚本期间,GUI 渲染线程挂起,被“冻结”。

JavaScript 引擎线程

也称 JS 内核,例如 V8 引擎,负责解析、运行 Javascript 脚本的虚拟机。

JS 引擎基于事件驱动单线程执行的,等待事件队列中的任务进来,然后处理。

这个唯一的 JS 线程也被称为主线程。

为什么 JS 是单线程的?

JS 会操作 DOM 树和 CSSOM 树来呈现动态交互和依赖服务器逻辑的交互,多线程同时处理同一个 DOM 节点,浏览器无法决定使用哪一个线程的处理结果,也无法决定处理的先后顺序,所以用单线程来避免。

JS 线程和 GUI 线程互斥也是因为如此,而且渲染线程可能会获取前后不一致的数据。

所以当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到引擎线程空闲时立即被执行。

如果 JS 执行事件过长,会导致页面渲染不连贯,也就是 JS 阻塞了页面加载

定时触发器线程

负责处理定时事件。

单线程处理任何事件都有可能发生阻塞,如果处于阻塞线程状态就会影响记计时的准确,所以计时器最好不存在在其他线程中,单独启用线程以确保计时的准确性。

事件触发线程

当浏览器触发事件时,事件触发线程会将其放在事件队列末尾,等待 JS 引擎空闲时处理。

这些事件可以是

  • JS 引擎正在执行的代码中的定时任务
  • 其他线程的事件:浏览器 DOM 事件,AJAX 请求等

异步 HTTP 请求线程

负责处理 HTTP 请求。

浏览器单独启用一个线程用于检测 XMLHttpRequest 连接后的状态变更。设有回调函数时,异步 HTTP 请求线程就将产生状态变更的事件放到JS引擎的处理事件队列中等待处理。

同步、异步和定时器执行

同步任务的执行

在主线程上排队执行的任务,只有前一个任务执行完毕,才能执行后一个任务。这些任务存放在执行栈中。

  • 输入输出
  • 变量声明、函数初始化、事件绑定
  • 同步函数:函数返回时可立即拿到结果。

异步任务的执行

未进入主线程,而是进入 FIFO 的事件队列排队执行。
异步任务准备好后触发某些事件通知主线程,并将回调函数返回给主线程。

  • 定时任务:setTimeout、setInterval
  • DOM事件:onClick、onScroll……
  • Promise
  • fs.readFile
  • http.get
  • 异步函数:函数返回时不能立即拿到结果,需要通过一些方式在未来某时拿到。

同步、异步执行优先级

异步任务挂起等待同步任务执行完再继续执行。

定时器执行

定时器执行是异步执行的。

setTimeout(fn, 500) 

定时器线程在 500ms 后将签名函数 fn 放在事件队列末尾,js 线程空闲后轮询执行事件队列。

HTML5 标准规定了 setTimeout() 的第二个参数的最小值,即最短间隔为4毫秒。如果低于这个值,就会自动增加。

在此之前,老版本的浏览器都将最短间隔设为10毫秒。

setTimeout 和 setInterval 的区别

setTimeout 只会执行一次。
setInterval 不清除会一直执行。

事件循环(Event Loop)

主线程将执行栈中的同步任务执行完毕后,执行栈空了,主线程从事件队列中读取新的压入执行栈并执行。若事件队列中没有任务则等待,直至有新任务。

这个过程是不断循环的,被称为事件循环。

@bencode
Copy link

bencode commented Jun 28, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

2 participants