闲谈 React 渲染机制

react 渲染机制总结为公式:

UI = F (state)

上面的公式还可以进行一个拆分:

  • 根据自变量(state) 的变化计算出 UI 的变化
  • 根据 UI 的变化执行具体的宿主环境的 API

对应公式

js 复制代码
const state = reconcile(update) // 通过 reconciler 计算出最新的状态
const UI = commit(state) // 根据上一步计算出来的state 渲染出UI

对应到 react 里就两个阶段

  • Render阶段:调合虚拟 DOM ,计算出最终要渲染出来的虚拟 DOM
  • Commit 阶段:根据上一步计算出来的虚拟 DOM, 渲染具体的 UI

每个阶段对应不同的组件:

  • 调度器(Scheduer) : 调度任务,为任务排序优先级,先优先级高的任务先到 Reconciler
  • 协调器(Reconciler) : 生成 Fiber 对象,收集副作用,找出哪些节点发生了变化,打上不同的 flags ,Diff算法也是这个组件中执行的
  • 渲染器(Rederer): 根据协调器计算出来的虚拟 DOM 同步的渲染节点到视图上。
jsx 复制代码
export default () => {
  const [count, setCount] = useState(0)

  return (
    <ul>
      <button onclick={() => setCount(count + 1)}> 乘 {count} </button>
      <li>{1 * count}</li>
      <li>{2 * count}</li>
      <li>{3 * count}</li>
    </ul>
  )
}

当用户点击按钮时,首先是由Scheduler进行任务的协调,render 阶段的工作流程是可以随时被以下原因中断:

  • 有其他更高优先级的任务需要执行
  • 当前的时间切片( time slice )没有剩余时间
  • 或者发生了其他错误

注意上面的 render 阶段的工作是在内存里进行的,不会更新宿主环境 UI,因此这个阶段工作流程反复被中断,用户也不会看到,用户也不会看到"更新不完整的 UI"

Scheduler 调度完成后,将任务交给 reconciler , reconciler 就需要计算出新的 UI,最有由 renderer 同步进行渲染更新操作。

调度器

在 React V16 版本以前,是采用 Stack 架构,所有任务只能同步进行,无法被打断,这就导致浏览器可能会出现丢帧的现象,表现出卡顿。react 为了解决这个问题, 从而在架构上进行了变更:

  • 引入 fiber
  • 新增了scheduler

Scheduler 在浏览器的原生 API 中实际上是有类似的实现的,这个 API 就是requestIdleCallback

虽然浏览器有类似的 API, 但是 react 团队并没有使用该 API , 因为该 API 存在兼容性问题,因此 react 团队自己实现了一套这样的机制,这个就是调度器Scheduler

协调器

协调器是 render 阶段的第二阶段工作,类组件或者函数组件本身就是这个阶段被调用的

根据 Scheduler 调度结果的不同,协调器起点可能是不同的

  • PerformSyncWorkOnRoot (同步更新流程)
  • performConcurrentWorkOnRoot (并发更新流程)
js 复制代码
// performSyncWorkRoot 会执行该方法
function workLoopSync() {
  while (workInProgress !== null) {
    performUnitOfWorkOnRoot(workInProgress)
  }
}
js 复制代码
// performConcurrentWorkOnRoot 会执行该方法
function workLoopSync() {
  while (workInProgress !== null && !shouldYield()) {
    performUnitOfWorkOnRoot(workInProgress)
  }
}

新的架构使用 Fiber 对象来描述 Dom 结构,最终需要形成一棵 Fiber tree, Fiber tree 通过链表的形式串联在一起,workInProgress 代表当前的 FiberNode

performUnitOfWork 方法会创建下一个 FiberNode , 并且还会将已创建的Fiber Node链接起来(child, return ,sibling),从而形成一个链表结构的 Fiber tree

如果 workInProgress 为 null , 说明已经没有下一个 fiberNode ,也就是说明整棵 Fiber tree 已经构建完毕

上面两个方法唯一的区别就是是否调用了 shouldYieId 方法,该方法表明了是否可以中断

perFormUnitOfWork 在创建下一个 FiberNode 时候,整体工作流程可以分为两大块

  • 递阶段

递阶段会从 HostRootFiber 开始向下以深度优先原则进行遍历,遍历到的每一个 FiberNode 执行 beginWork 方法。该方法会根据传入的 Fiber Node 创建下一级 Fiber Node , 此时存在两种情况:

  • 下一级只有一个元素,beginWork 方法会创建对应的 FiberNode , 并于 workInProgress 连接
jsx 复制代码
<ul>
  <li></li>
</ul>

这里就会创建 li 对应的 FiberNode ,做出如下的连接

js 复制代码
LiFiber.return = UIFiber
  • 下一级有多个元素,这是 beginWork 方法会一次创建所有的子 FiberNode 并且通过 siblings 连接到一起,每个子FiberNode也会和workInProgress连接
jsx 复制代码
<ul>
  <li></li>
  <li></li>
  <li></li>
</ul>

此时会创建 3 个 li 对应的 FiberNode , 连接情况如下

js 复制代码
// 所有的子 Fiber 依次连接
Li0Fiber.sibling = Li1Fiber
Li1Fiber.sibling = Li2Fiber

// 子 Fiber 还需要和父 Fiber 连接
Li0Fiber.return = UIFiber
Li1Fiber.return = UIFiber
Li2Fiber.return = UIFiber

由于采用的是深度优先的原则,因此无法再往下走的时候,会进入到归阶段

  • 归阶段

归阶段会调用 completeWork 方法来处理 FiberNode , 做一些副作用收集。 当每个 FiberNode 执行完了 completeWork 方法后,存在兄弟元素,就会进入到兄弟元素的递阶段,如果不存在兄弟元素,就会进入父 FiberNode 的归阶段

jsx 复制代码
function performUnitOfWork(fiberNode) {
  // ... beginWork
  if (fiberNode.child) {
    performUnitOfWork(fiberNode.child)
  }

  // ... completeWork
  if (fiberNode.sibling) {
    performUnitOfWork(fiberNode.sibling)
  }
}

渲染器 (Renderer)

属于 commit 阶段,该阶段会将各种副作用 commit 到宿主环境 UI 中. 相比较于之前的render阶段可以被打断,commit 阶段一旦开始,就会同步执行,直到完成渲染工作

渲染过程中可以分为 3 个子阶段:

  • BeforeMutation 阶段
  • Mutation 阶段
  • Layout 阶段
总结:

React 整体的渲染流程可以分为两大阶段,分别是 render 阶段和 commit 阶段。render 阶段里面会经由调度器协调器处理,此过程是在内存中 运行,是异步可中断的commit 阶段是由渲染器进行处理,根据副作用进行 UI 的更新,此过程是同步不可中断的,否则会造成 UI 和数据显示不一致。

调度器: 调度器的主要工作就是调度任务,让所有的任务有优先级的概念,这样的话紧急的任务可以优先执行,Scheduler 实际上在浏览器的 API 中是有原生实现的,这个 API 叫做 requestIdleCallback,但是由于兼容性问题,React 放弃了使用这个 API,而是自己实现了一套这样的机制

协调器: 协调器是 Render 的第二阶段工作。该阶段会采用深度优先的原则遍历并且创建一个一个的 FiberNode,并将其串联在一起,在遍历时分为了"递与归"两个阶段,其中在递阶段会执行 beginWork 方法,该方法会根据传入的 FiberNode 创建下一级 FiberNode。而"归"阶段则会执行 CompleteWork 方法,做一些副作用的收集

渲染器: 渲染器的工作主要就是将各种副作用 (fags 表示) commit 到宿主环境的 UI 中。整个阶段可以分为三个子阶段,分别是 BeforeMutation 阶段、Mutation 阶段和 Layout 阶段。

相关推荐
海盐泡泡龟2 小时前
“组件、路由懒加载”,在 Vue3 和 React 中分别如何实现? (copy)
前端·javascript·react.js
国家不保护废物4 小时前
🧩 React 组件化进阶:像乐高大师一样搭建你的应用世界!
前端·react.js·ai编程
TimelessHaze4 小时前
从"切图崽"到前端工程师:React 到底是个啥?🚀
前端·react.js·ai编程
Java陈序员4 小时前
再见 Navicat!一款开源的 Web 数据库管理工具!
java·react.js·docker
HarderCoder4 小时前
ByAI:Rect-redux实现及connect函数
前端·react.js
小满zs6 小时前
Zustand 第三章(状态简化)
前端·react.js
星辰引路-Lefan16 小时前
深入理解React Hooks的原理与实践
前端·javascript·react.js
飞鸟malred17 小时前
vite+tailwind封装组件库
前端·react.js·npm
TE-茶叶蛋17 小时前
Vue Fragment vs React Fragment
javascript·vue.js·react.js