本文只是用于将学习到的知识做一个梳理与总结
浏览器架构
现代浏览器通常采用多进程架构。每个进程都有独立的内存空间,相互隔离,提高浏览器的稳定性、安全性和性能。
以Chrome为例,浏览器的进程包含以下几个主要进程:
- 浏览器主进程: 负责协调整个浏览器的运行,包括用户界面、网络请求、子进程的创建和销毁等。
- 渲染进程: 将HTML/CSS/JS转化为用户可以交互的网页
- 网络进程: 处理网络请求、响应、DNS等
- GPU进程:负责处理图形渲染相关的任务,如2D、3D绘图等
- 插件进程:运行浏览器插件
渲染进程
对于我们的页面来说,最重要的就是渲染进程
,它包含了以下的多个线程
:
- 主线程:负责处理用户输入、JavaScript 执行和页面布局计算等任务,是渲染进程中最重要的线程之一。
- 渲染线程:负责将 HTML、CSS 和 JavaScript 转换为可视化的页面,其中包括页面布局、样式计算、绘制和合成等任务。
- 合成线程:负责将页面中的多个图层合成为最终的显示内容,并将其发送到 GPU 进行渲染。
- JavaScript引擎线程:JavaScript 引擎线程负责解析和执行页面中的 JavaScript 代码
- 事件线程:负责处理用户输入事件,如鼠标点击、键盘输入等,以及页面中的事件触发和处理
- IO线程: 负责接收其他进程传进来的消息
- …
在这当中,主线程最为繁忙,既要处理 DOM,又要计算样式,还要处理布局,同时还需要处理 JavaScript 任务以及各种输入事件。而浏览器则通过在主线程
中实现消息队列
和事件循环系统
来调度这么多不同类型的任务。
我们可以通过下面的图片来了解主线程
、事件循环
、消息队列
和其他线程之间的关系
但是,消息队列
是先进先出的。主线程
所有执行任务都来自于消息队列
。会面临以下两个问题
1. 如何处理高优先级的任务
比如,如何监控DOM节点的变化情况(节点的插入、修改、删除等动态变化),然后根据变化来处理相应的业务逻辑。一个通用设计就是利用js设计一套监听接口,当变化发生时,渲染引擎同步调用这些接口,这是一个典型的观察者模式。
不过这个模式有个问题,因为 DOM 变化非常频繁,如果每次发生变化的时候,都直接调用相应的 JavaScript 接口,那么这个当前的任务执行时间会被拉长,从而导致执行效率的下降。如果将这些 DOM 变化做成异步的消息事件,添加到消息队列的尾部,那么又会影响到监控的实时性,因为在添加到消息队列的过程中,可能前面就有很多任务在排队了。
这也就是说,如果 DOM 发生变化,采用同步通知的方式,会影响当前任务的执行效率;如果采用异步方式,又会影响到监控的实时性。
2. 如何解决单个任务执行时长过久的问题。
从图中可以看到,所有任务都是在单线程中执行的,而由于每一帧的时间有限,如果某一个js任务非常的耗时,那么下面的任务(DOM解析、JS事件、布局计算、用户输入事件等)就需要等待很长时间。这也就是我们页面中卡顿的由来。
第一个问题就可以通过下面的微任务
来解决
宏任务,微任务
首先,我们需要知道任务队列
中包含有以下两种类型的任务
宏任务
- 渲染事件(如解析 DOM、计算布局、绘制);
- 用户交互事件(如鼠标点击、滚动页面、放大缩小等);
- JavaScript 脚本执行事件;
- 网络请求完成、文件读写完成事件。
-
setTimeout
的回调函数属于宏任务
微任务
-
Promise
的回调属于微任务
-
MutationObserver
的回调函数:当被观察的 DOM 节点发生变化时,MutationObserver
的回调函数会被添加到微任务队列中。 -
queueMicrotask
方法:该方法可以将回调函数添加到微任务队列中,等待执行。该方法是 ES2020 标准中新增的。
宏任务
与微任务
的最主要区别在于它们的执行时机
宏任务
是添加一个新的任务到消息队列
中,如果使用setTimeout
来异步执行一个操作时,时间间隔无法精准掌控,对于一些高实时性
的需求不太符合。比如你在程序中使用setTimeout
延迟1000ms
去执行某个任务时,可能在这1000ms
中已经触发了很多系统级的任务,它们已经被插入到了消息队列
中。等到过了1000ms
后setTimeout
才将会它的回调插入到消息队列
中,这就需要等待队列前的任务全部执行完了才能到它的回调
微任务
是 在当前宏任务
结束前再执行微任务
,每个宏任务
都关联了一个微任务队列
。所以,只要在当前宏任务
中触发了微任务
,所有微任务的回调都会被添加到微任务队列
中等待执行。这样,你再怎么交互,生成的宏任务
都会排在当前的宏任务
之后。这样,实时性
问题就解决了。
React如何利用浏览器的特性来做“并发”
在了解了前面关于浏览器的特性以及相关问题后。我们再回到react中看React为了并发特性做了哪些改动。
time slice 与 fiber
在react16之前,一直是递归更新。而16之后,react提出了一个新的概念 time slice,便于将任务切分,然后在浏览器的空闲时间来执行任务,超出了空闲时间则将剩余任务往后推。但由于递归更新中断后无法再继续,所以react重构了它的代码,将递归更新改成了fiber这种链表结构。这样即使是暂停了,还能从暂停出的链表继续执行。这样就解决了组件单个执行任务过长
的问题。
异步更新
我们可以在react的react-reconciler包中找到scheduleSyncCallback
方法,所有的更新操作
都保存到了syncQueue
队列中,然后通过scheduleMicrotask
这个方法创建微任务,flushSyncCallbacks
就是这个微任务的异步回调,而flushSyncCallbacks
当中执行的就是所有的更新操作
。这就解决了组件更新效率
的问题。
Scheduler 调度器
现在,有了可中断的任务,并且同步任务
被放到了微任务
中执行。而且因为一般主流浏览器刷新频率为60Hz,即每16.6ms(1000ms / 60Hz)浏览器刷新一次。
所以react需要解决的就是如何利用每一帧中预留给js线程的时间来更新组件(在scheduler源码中,react预留了5ms)。当超过预留时间后,react就会中断更新,等待下一帧的空闲时间继续从被中断的fiber
处执行。这样就尽可能的避免了任务执行时间过长而出现掉帧
、卡顿
的现象。
总结
react 利用浏览器的渲染进程主线程的事件循环
以及宏任务
、微任务
的特点,将原有的数据结构改变为Fiber
这种可中断的链表结构。
并且通过将所有的更新操作
使用微任务
来执行,解决组件更新的实时性
问题。然后再实现了调度器
来完成任务的中断和继续来解决任务执行时间过长
的问题。
参考
- 浏览器工作原理与实践
- react技术揭秘
- 从零实现React 18
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net