react div 事件优先级_React 架构的演变 更新机制

前面的文章分析了 Concurrent 模式下异步更新的逻辑,以及 Fiber 架构是如何进行时间分片的,更新过程中的很多内容都省略了,评论区也收到了一些同学对更新过程的疑惑,今天的文章就来讲解下 React Fiber 架构的更新机制。

Fiber 数据结构

我们先回顾一下 Fiber 节点的数据结构(之前文章省略了一部分属性,所以和之前文章略有不同):

function FiberNode (tag, key) {

缓存机制

可以注意到 Fiber 节点有个 alternate 属性,该属性在节点初始化的时候默认为空(this.alternate = null)。这个节点的作用就是用来缓存之前的 Fiber 节点,更新的时候会判断 fiber.alternate 是否为空来确定当前是首次渲染还是更新。下面我们上代码:

import React 

在调用 createRoot 的时候,会先生成一个FiberRootNode,在 FiberRootNode 下会有个 current 属性,current 指向 RootFiber 可以理解为一个空 Fiber。后续调用的 render 方法,就是将传入的组件挂载到 FiberRootNode.current(即 RootFiber) 的空 Fiber 节点上。

// 实验版本对外暴露的 createRoot 需要加上 `unstable_` 前缀

render 最后调用 scheduleUpdateOnFiber 进入更新任务,该方法之前有说明,最后会通过 scheduleCallback 走 MessageChannel 消息进入下个任务队列,最后调用 performConcurrentWorkOnRoot 方法。

// scheduleUpdateOnFiber

开始更新时,如果 workInProgress 为空会指向一个新的空 Fiber 节点,表示正在进行工作的 Fiber 节点。

workInProgress.alternate = current
current.alternate = workInProgress
28fb8aab543297a7c3c627f661e38aac.png
fiber tree

构造好 workInProgress 之后,就会开始在新的 RootFiber 下生成新的子 Fiber 节点了。

function renderRootConcurrent(root) {

按照我们前面的案例, workLoopConcurrent 调用完成后,最后得到的 fiber 树如下:

class App extends React.Component {
0fcbbe05c612b4fe3a7c7255856ca206.png
fiber tree

最后进入 Commit 阶段的时候,会切换 FiberRootNode 的 current 属性:

function performConcurrentWorkOnRoot() {
012a510e975bfb84d5556db669a60fb3.png
fiber tree

上面的流程为第一次渲染,通过 setState({ val: 1 }) 更新时,workInProgress 会切换到 root.current.alternate

function createWorkInProgress() {
2ee0aba02ab7a3ee344ce6b3711264db.png
fiber tree

在后续的遍历过程中(workLoopConcurrent()),会在旧的 RootFiber 下构建一个新的 fiber tree,并且每个 fiber 节点的 alternate 都会指向 current fiber tree 下的节点。

4e44893e7ebb9ed6da3dd02cf00ada5b.png
fiber tree

这样 FiberRootNode 的 current 属性就会轮流在两棵 fiber tree 不停的切换,即达到了缓存的目的,也不会过分的占用内存。

更新队列

在 React 15 里,多次 setState 会被放到一个队列中,等待一次更新。

// setState 方法挂载到原型链上

同样在 Fiber 架构中,也会有一个队列用来存放 setState 的值。每个 Fiber 节点都有一个 updateQueue 属性,这个属性就是用来缓存 setState 值的,只是结构从 React 15 的数组变成了链表结构。

无论是首次 Render 的 Mount 阶段,还是 setState 的 Update 阶段,内部都会调用 enqueueUpdate 方法。

// --- Render 阶段 ---

enqueueUpdate 方法的主要作用就是将 setState 的值挂载到 Fiber 节点上。

function enqueueUpdate(fiber, update) {

多次 setState 会在 sharedQueue.pending 上形成一个单向循环链表,具体例子更形象的展示下这个链表结构。

class App extends React.Component {

点击 div 之后,会连续进行三次 setState,每次 setState 都会更新 updateQueue。

5529932ededd6c6e2fdf08cdad7f356a.png
第一次 setState
4fc96c0ff4341fbc6e3816f4abe782a8.png
第二次 setState
6184f625fbdf008ce868743b52309e8b.png
第三次 setState

更新过程中,我们遍历下 updateQueue 链表,可以看到结果与预期的一致。

let $pending = sharedQueue.pending
67d4ba3f2d5e4c1ecc174d559a124ff4.png
链表数据

递归 Fiber 节点

Fiber 架构下每个节点都会经历递(beginWork)归(completeWork)两个过程:

  • beginWork:生成新的 state,调用 render 创建子节点,连接当前节点与子节点;
  • completeWork:依据 EffectTag 收集 Effect,构造 Effect List;

先回顾下这个流程:

function workLoopConcurrent() {

递(beginWork)

先看看 beginWork 进行了哪些操作:

function beginWork(current, workInProgress) {

首先判断 current(即:workInProgress.alternate) 是否存在,如果存在表示需要更新,不存在就是首次加载,didReceiveUpdate 变量设置为 false,didReceiveUpdate 变量用于标记是否需要调用 render 新建 fiber.child,如果为 false 就会重新构建fiber.child,否则复用之前的 fiber.child

然后会依据 workInProgress.tag 调用不同的方法构建  fiber.child。关于 workInProgress.tag 的含义可以参考 react/packages/shared/ReactWorkTags.js,主要是用来区分每个节点各自的类型,下面是常用的几个:

var FunctionComponent = 

调用的方法不一一展开讲解,我们只看看 updateClassComponent

// 更新 class 组件

首先遍历了之前提到的 updateQueue 更新 state,然后就是判断 state 是否更新,以此来推到组件是否需要更新(这部分代码省略了),最后调用的组件 render 方法生成子组件的虚拟 DOM。最后的 reconcileChildren 就是依据 render 的返回值来生成 fiber 节点并挂载到 workInProgress.child 上。

// 构造子节点

篇幅有限,看看 render 返回值为对象的情况(通常情况下,render 方法 return 的如果是 jsx 都会被转化为虚拟 DOM,而虚拟 DOM 必定是对象或数组):

if (

归(completeWork)

fiber.child 为空时,就会进入 completeWork 流程。而 completeWork 主要就是收集 beginWork 阶段设置的 effectTag,如果有设置 effectTag 就表明该节点发生了变更, effectTag  的主要类型如下(默认为 NoEffect ,表示节点无需进行操作,完整的定义可以参考 react/packages/shared/ReactSideEffectTags.js):

export 

我们看看 completeWork 过程中,具体进行了哪些操作:

function completeWork(current, workInProgress) {

beginWork 一样,completeWork 过程中也会依据 workInProgress.tag 来进行不同的处理,其他类型的组件基本可以略过,只用关注下 HostComponentHostText,这两种类型的节点会反应到真实 DOM 中,所以会有所处理。

function (
 current, workInProgress, type, newProps) {

updateHostComponent 方法最后会通过 diffProperties 方法获取一个更新队列,挂载到 fiber.updateQueue 上,这里的 updateQueue 不同于 Class 组件对应的 fiber.updateQueue,不是一个链表结构,而是一个数组结构,用于更新真实 DOM。

下面举一个例子,修改 App 组件的 state 后,下面的 span 标签对应的 data-valstylechildren 都会相应的发生修改,同时,在控制台打印出 updatePayload 的结果。

import React 
ef057eb72c557a79d391aabff892a0fb.png
console

副作用链表

在最后的更新阶段,为了不用遍历所有的节点,在 completeWork 过程结束后,会构造一个 effectList 连接所有 effectTag 不为 NoEffect 的节点,在 commit 阶段能够更高效的遍历节点。

function completeUnitOfWork() {

上面的代码就是构造 effectList 的过程,光看代码还是比较难理解的,我们还是通过实际的代码来解释一下。

import React 
7ae6a798ee1b5b3438da2df8af6ee13e.png
App

我们构造一个 2 * 2 的 Table,每次点击组件,td 的 children 都会发生修改,下面看看这个过程中的 effectList 是如何变化的。

第一个 td 完成 completeWork 后,EffectList 结果如下:

3c39fcddbe5a4511c9ad09f520d2da8a.png
1

第二个 td 完成 completeWork 后,EffectList 结果如下:

d47e0b45d170fbb6d2cd0dab5b9b6e0f.png
2

两个 td 结束了 completeWork 流程,会回溯到 tr 进行 completeWork ,tr 结束流程后 ,table 会直接复用 tr 的 firstEffect 和 lastEffect,EffectList 结果如下:

acdcd1b83272f5112a06c5682eff3969.png
3

后面两个 td 结束 completeWork 流程后,EffectList 结果如下:

30d6f293757024787864be43ef37f66c.png
4

回溯到第二个 tr 进行 completeWork ,由于 table 已经存在 firstEffect 和 lastEffect,这里会直接修改 table 的 firstEffect 的 nextEffect,以及重新指定 lastEffect,EffectList 结果如下:

32f7641262f64d7f1fd48ab12adda7c7.png
5

最后回溯到 App 组件时,就会直接复用 table 的 firstEffect 和 lastEffect,最后 的EffectList 结果如下:

a931b61e8adcdde1ff1af9a3172bda09.png
6

提交更新

这一阶段的主要作用就是遍历 effectList 里面的节点,将更新反应到真实 DOM 中,当然还涉及一些生命周期钩子的调用,我们这里只展示最简单的逻辑。

function commitRoot(root) {

这里不再展开讲解每个 effect 下具体的操作,在遍历完 effectList 之后,就是将当前的 fiber 树进行切换。

function commitRoot() {

总结

到这里整个更新流程就结束了,可以看到 Fiber 架构下,所有数据结构都是链表形式,链表的遍历都是通过循环的方式来实现的,看代码的过程中经常会被突然出现的 return、break 扰乱思路,所以要完全理解这个流程还是很不容易的。

最后,希望大家在阅读文章的过程中能有收获,下一篇文章会开始写 Hooks 相关的内容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值