最近跟着 B 站 up 主付金权学习到的一点笔记,写的比较零散,只做辅助理解。
1.首先得知道啥是渲染帧
- 帧:一帧画面,类似于游戏掉帧,卡
- 帧率:切换画面的速度,浏览器一般也就 1 秒 60 帧,一帧执行 16 毫秒
- 掉帧:由于前一帧的效率问题,把后面的帧挤占掉了
比如一些交互事件,由于前一帧的占用时间,导致 input 框输入的内容无法及时触发
如果想稳定浏览器的帧率,就要确保一帧做的事情不要太多
同样多的代码量,在 vue 里首屏渲染可能会掉帧,但是 react 就不会,因为 react 有调度机制,优先级概念,而且 react 的渲染是可以中断的(concurrency)
2.React 是咋处理渲染的
大致分为两个模块:
- fiber 架构:里面主要是虚拟 DOM 和 Diff 算法
- concurrency(并发性,可中断渲染):react18 以前叫 concurrency mode,18 以后更名为 concurrency
首先要知道树形结构和数组还有对象的性质都是一样的,都是用来表示数据关系的性质
初次渲染一个组件到页面的步骤:
- 拿到 React.createElement 返回的节点,也就是一个对象,类似于生成的 vdom
1
2
3const rootElement = React.createElement("div", { className: 'wrapper' },
React.createElement("span", {}. "hello,text"),
React.createElement("myCount", { type: 'component' } )) - 通过 render 方法进行渲染
- 主要分为
- 如果是组件节点,则会在执行渲染的过程中保存对应的 hooks 以及触发的对应 hooks (比如 useState 立即触发,useEffect 要等 dom 挂载完触发)
- 如果是 react 元素节点(div,span),不会生成真实 dom 而是生成一个表述对象【这个描述对象描述的需要创建真实 dom 的一些信息,以及这个对象要做的操作:create】这个描述对象就叫做 fiber
- 通过这个描述对象的清单,将内部依次编译为真实 dom,然后插入父元素的子节点,appendChild
- 等整个流程结束后,得到一个真实的 dom 树,最后将其插入到对应的 root 元素下面
- 触发对应的生命周期事件
更新组件渲染步骤:
- 生成一个新的节点,但不会全部重新生成,只重新生成相关组件和有依赖的元素
- 直接进入 diff 比较,然后也会生成一个描述对象的清单【里面也都是 fiber 节点,每个节点的状态可以是 create / delete / update 中的一个】
- 将差异点应用到真实 dom ,触发对应的生命周期事件
当代码量足够多时,那么去执行 React.createElement 和 Render 这两个方法的时间就越长,但是浏览器一帧就 16 毫秒,如果超过了,就掉帧,用户交互失败
其中最耗时的就是 我们的代码 和 react 的逻辑代码,反而创建 dom 是最快的
所以 react 就帮我们分成了两个阶段
- render 阶段,执行我们和 react 的逻辑代码,并生成一个描述对象以便告诉下一阶段的执行【fiber 清单,类似表格,指定在哪里插入 dom 和删除 dom,记录了最终要展示给用户的 dom 树是什么样的】
- commit 阶段,将虚拟 dom 映射到真实 dom
既然已经将整个渲染流程分为了两个阶段,那我们如果需要更好的性能,避免丢帧,我们就要对 render 阶段做点文章,这个时候就需要 concurrency 出场了
concurrency(并发性,可中断渲染)
首先掉帧在首次渲染一般是无感知的,因为此时页面不可交互,而在更新渲染时,因为可以在原页面上交互,所以用户能感知到,这个时候先介绍两个方法
1 | // 一帧内必定要执行的函数 |
1 | // 每一帧还有剩余时间的时候执行 |
concurrency 可中断渲染的实现
核心就是递归,每次的渲染函数渲染前都会通过两层判断,是否还有任务需要执行 / 是否还有额外的执行时间,通过函数里 deadline 参数的 timeRemaining 函数判断剩余执行时间,不够的话就通过 requestIdleCallback 推向下一帧执行,这样无论组件写的有多大都可以拆分到每一帧避免掉帧。
后续通过前面两层判断后开始真正的节点渲染,如果 type 是 function 就代表是组件,其它类型则为 dom 真实元素直接渲染,但是此时还不会马上挂载到页面上,此时还在 render 阶段
Tips:concurrency 拆分的最小单元是组件和 react 元素
3.useTransition 的妙用
首先要知道 fiber 优先级:react 除了会把任务拆分为很多块以外【可中断渲染】,还会做一个事情,就是将任务分成不同的优先级执行
比如在一个切换 tab 的场景下,某一个 tab 组件的渲染超过了 16ms 造成掉帧导致切换 tab 不流畅,这个时候我们希望切换 tab 的这个行为优先级 高于页面的渲染优先级,中断当前切换 tab 的渲染工作,这个时候 useTransition 就可以做到
1 | // pending 状态,代表当前是否有 transition 任务在执行 |