一文搞懂React18原理

文章目录

  • 前言
  • React 初始化过程
    • 节点类型
    • 首次渲染阶段
  • React 更新过程
    • 更新场景
    • 对比更新过程(diff)
    • key 作用
  • React Fiber 架构
    • 单线程 CPU 调度策略
    • 分片设计
      • 前端如何解决
      • React 是如何优化的
      • 时间分片
      • requestIdleCallback 和 requestAnimation 区别
  • React 核心包结构
    • 核心包
    • React 工作循环
  • React 核心对象
    • ReactElement 对象
    • fiber 对象
    • UpdateQueue 与 UpdateQueue
    • Hook 对象
    • Task 对象
  • reconciler
    • reconciler 执行流程
    • reconciler 启动过程
      • 创建全局对象
      • 创建 ReactDOM Root 对象
      • 创建 fiberRoot 对象
      • 创建 HostRootFiber 对象
      • 可中断渲染
  • 优先级管理
    • Lane(车道模型)
    • Lane 的位运算
      • 位运算在 react 中的运用
    • 优先级区别和联系
  • React 调度原理
    • 调度实现
    • 内核
    • 任务队列管理
  • fiber 树构建
    • ReacElement,Fiber,DOM 三者关系
    • 双缓冲技术(double buffering)
    • 优先级
      • update 优先级(update.lane)
      • 渲染优先级(renderLanes)
      • fiber 优先级
    • 栈帧模型


前言

可能一直在写代码的路上,大多数时候就是个工具人。今天一起看看react18的实现原理,持续学习中。


React 初始化过程

react 在渲染过程中要做很多事,不会直接通过初始元素直接渲染。还有虚拟节点。除了初始元素能生成虚拟节点外,还有哪些能生成虚拟节点?

节点类型

  1. DOM 节点:虚拟 DOM 节点。当初始元素的 type 为字符串的时候 react 就会创建虚拟 DOM 节点
  2. 组件节点:当初始元素的 type 为函数或是类的时候,react 就会创建虚拟组件节点
  3. 文本节点:直接书写字符串或者数字,react 会创建文本节点
  4. 空节点:react 代码中的三目表达式 a>b?1:false,用来条件渲染,当为 false 时不会渲染。其实遇到字面量 null,false,true,undefined 在 react 中均会被创建为一个空节点,在渲染时遇到空节点将什么都不会渲染
  5. 数组节点:不是直接渲染数组本身,当 react 遇到数组时会创建数组节点,但是不会直接进行渲染,而是将数组的每一项拿出来,根据不同的节点类型去做相应的事情。所以数组里的每一项只能是上面这五种节点类型

react 工作是通过初始元素或者可以生成虚拟节点的东西生成虚拟节点,然后针对不同的节点类型去做不同的事情最终生成 DOM 挂载到页面上。

首次渲染阶段

1.初始元素—DOM 节点

针对初始元素的 type 属性为字符串时,react 会通过 document.createElement 创建真实 DOM。因为初始元素的 type 为字符串,所以会根据 type 属性创建不同的真实 DOM。创建完真实 DOM 后会立即设置该真实 DOM 的属性,比如直接在 jsx 中可以直接书写 className,style 等等都会作用到真实的 DOM 上。

2.初始元素—组件节点

如果初始元素的 type 属性是一个 class 类或者 function 函数时,那么会创建一个组件节点。所以针对类或函数组件,处理是不同的。

函数组件

对于函数组件会直接调用函数,将函数的返回值进行递归处理(看看是什么节点类型,然后去做对应的事情,所以一定要返回能生成虚拟节点的东西),最终生成一颗 vDom 树。

类组件

对于类组件而言相对会麻烦

  • 首先创建类的实例(调用 constructor)

  • 调用生命周期方法 static.getDerivedStateFromProps

  • 调用生命周期方法 render,根据返回值递归处理,跟函数组件处理返回值一样,最终生成一颗 vDom 树

  • 将组件的生命周期方法 componentDidMount 加入到队列中等待真实 DOM 挂载到页面后执行(前面说 render 是一个递归处理,所以如果一个组件存在父子关系的时候,那么肯定要等到子组件渲染完父组件才能走出 render,所以子组件的 compontDidMount 一定是比父组件先入队列的,肯定先运行)

    3.文本节点

针对文本节点,会直接通过 document.createTexNode 创建真实的文本节点

4.空节点

如果生成的是空节点,那么将什么都不会做

5.数组节点

react 不会直接渲染数组,而是将数组的每一项拿出来遍历,根据不同的节点类型去做相应的事情,直到递归处理完数组里的每一项

总结:处理完所有的节点后,我们的 vDom 树和真实 DOM 也会创建好,react 会将 vDom 树保存起来,方便后续使用。然后将真实 DOM 都挂载到页面上。

React 更新过程

更新场景

1.组件更新(setState)

经常用 setState 来重新设置组件的状态进行重新渲染。使用 setState 只会更新调用此方法的类。不会设计到兄弟节点以及父级节点。影响范围仅仅是自己的子节点,步骤如下:

  1. 运行当前类组件的生命周期方法 static.getDerivedStateFromProps。根据返回值合并当前组件的状态
  2. 运行当前类组件的生命周期方法 shouldComponentUpdate。如果该方法返回 false,直接终止更新流程
  3. 运行当前类组件的生命周期方法 render,得到一个新的 vDom 树,进入新旧两棵树的对比更新
  4. 将当前类组件的生命周期方法 getSnapshotBeforeUpdate 加入执行队列,等待将来执行
  5. 将当前类组件的生命周期方法 componentDidUpdate 加入执行队列,等待将来执行
  6. 重新生成 vDom 树
  7. 执行队列,此队列存放的是更新过程设计到原本存在的类组件生命周期方法 getSnapshotBeforeUpdate
  8. 根据 vDom 树更新真实 DOM
  9. 执行队列,此队列存放的是更新过程中所涉及到原本存在的生命周期方法 componentDidUpdate
  10. 执行队列,此队列存放的是更新过程中所有卸载类组件的生命周期方法 componentWillUnMount

2.根节点更新(ReactDOM.createRoot().render)

在 ReactDOM 的新版中,不在直接使用 ReactDOM.render 进行更新,而是通过 createRoot(要控制的 DOM 区域)的返回值来调用 render

对比更新过程(diff)

对比更新就是将新 vDom 树和之前首次渲染过程中保存的老 vDom 树对比发现差异后做一系列操作的过程。

React 的 diff 算法将之前的复杂度 O(n^3)降为了 O(n),它做了以下几个假设

  1. 假设此次更新的节点层级不会发生移动(直接找到就树中的位置对比)
  2. 兄弟节点之间通过 key 进行唯一标识
  3. 如果新旧节点类型不相同,那么它认为就是一个新的结构,比如之前就是初始元素 div 现在变成初始元素 span 那么它会认为整个结构全部变了,无论嵌套了多深也会全部丢弃重新创建

key 作用

为了通过旧节点,寻找对应的新节点进行对比提高节点的复用率。

React Fiber 架构

单线程 CPU 调度策略

1.先到先得(First-Come-First-Server,FCFS)

最简单调度策略,简单说就是没有调度。谁先来谁就先执行,如果中间某些进程因为 I/O 阻塞,这些进程会挂起移回就绪队列(重新排队)

2.轮转调度

基于时钟的抢占策略,也是抢占策略中最简单的一种:公平的给每一个进程一定的执行时间,当时间消耗完毕或阻塞,操作系统就会调度其他进程,将执行权抢占过来

要点是确定合适的时间片长度:太长了,长进程霸占太久资源,其他进程得不到响应(等待时间过长),太短了,进程抢占和切换都是需要成本的,而且成本不低,时间片太短,时间浪费给在上下文切换上,导致进程干不了什么实事。

因此,时间片的长度最好符合大部分进程完成一次典型交互所需的时间

3.最短进程优先(Shortest Process Next,SPN)

按照进程的预估执行时间对进程进行优先级排序,先执行完短进程,后执行长进程,这是一种非抢占策略

SPN 缺点:如果系统有大量短进程,那么长进程可能会饥渴得不到响应

4.最短剩余时间(Shortest Remaining Time,SRT)

5.最高响应比优先(HRRN)

6.反馈法

分片设计

前端如何解决

  1. 优化每个任务,让它有多快就多快,挤压 cpu 运算量
  2. 快速响应用户,让用户觉得够快,不能阻塞用户的交互(React 分片)
  3. 尝试 Worker 多线程

Vue 选择的是 1,使用模板让它有了很多优化的空间,配合响应式机制可以让 Vue 精确的进行节点更新。

React 选择的是 2。

对于 Worker 多线程渲染方案有人尝试,但是要保证状态和试图的一致性相当麻烦

React 是如何优化的

为了给用户一种应用很快的假象,不能让一个程序长期霸占资源,可以将浏览器的渲染、布局、绘制、资源加载、事件响应、脚本执行视作操作系统的进程,我们需要通过某些调度策略合理的分配 CPU 资源,从而提高浏览器的用户响应速度,同时兼顾任务执行效率。

react 通过 fiber 架构,让自己的 reconcilation 过程变成可被中断的。适时的让出 CPU 执行权,除了可以让浏览器及时的响应用户的交互,还有其他好处。

时间分片

人眼最高识别帧数不超过 30 帧,电源帧数大多固定在 24,浏览器最优的帧率是 60,即 16.5ms 左右渲染一次

但是当 JS 执行时间过长,FPS(每秒显示帧数)下降造成视觉上的卡顿

如何解决,就是 fiber reconciler 要做的事。将要执行的 JS 做分片,保证不会阻塞主线程(Main thread)即可

requestIdleCallback 和 requestAnimation 区别

requestIdleCallback 是在空闲时间执行

requestAnimation 是在下一帧渲染之前执行

React 核心包结构

核心包

1.react

react 基础包,提供 react 组件(ReactElement)的必要函数,一般来说(react-dom,react-native)一同使用,在编写 react 应用的代码时,大部分都是调用此包 api

2.react-dom

react 渲染器之一,是 react 和 web 平台连接的桥梁,将 react-reconeiler 中的运行结果输出到 web 界面,在编写 react 应用时,大多数场景下能用到此包的就是一个入口函数 ReactDOM.render

3.react-reconciler

react 得以运行的核心包(综合协调 react-dom,react,scheduler 各包之间的调用与配合),管理 react 应用状态的输入和输出,将输入信号最终转换成输出信号传递给渲染器

4.react-scheduler

调度机制的核心实现,控制有 react-reconciler 送入回调函数的执行时机,在 concurrent 模式 下可以实现任务分片,在编写 react 应用的代码时,同样几乎不会直接用到此包提供的 api

  • 核心任务就是执行回调(回调函数由 react-reconciler 提供)
  • 通过控制回调函数的执行时机,来达到任务分片的目的,实现可中断渲染(concurrent 模式下才有此特性)

React 工作循环

react 核心就是下面两大工作循环

1.react-reconciler

核心:构建 fiber 树,生成任务

2.scheduler

核心:任务调度,任务优先级

React 核心对象

ReactElement 对象

在 jsx 语法中书写的节点,都会变编译器转换,最终会以 React.createElement(…)的方式创建出来一个 ReactElement 对象

主要 2 个属性:

1.key:在 reconciler 阶段会用到,默认值是 null,在 diff 算法中会使用到

2.type:这个属性决定了节点的种类:

在 reconciler 阶段会根据 type 执行不同的逻辑

它的值可以是字符串(div,span 等 dom 节点),函数(function,class 等节点),或者 react 内部定义的节点类型(portal,context,fragment 等)

  • 如 type 是字符串类型,则直接使用
  • 如 type 是 ReactComponent 类型,则会调用 render 方法获取子节点
  • 如 type 是 function 类型,则会调用该方法获取节点

注意:

  • class 和 function 类型的组件,其子节点是在 render 之后(reconciler 阶段)才生成的
  • 父级对象和子级对象通过 props.children 属性进行关联的(与 fiber 树不同)
  • ReactElement 虽然不能算是一个严格的树,也不能算是一个严格的链表,他的生成过程是自顶向下的,是所有的组件节点的总和
  • ReactElement 树和 fiber 树是以 props.children 为单位先后交替生成的,当 ReactElement 树构建完成,fiber 树也随后构建完毕
  • reconciler 阶段会根据 ReactElement 的类型生成对应的 fiber 节点(不是一 一对应的,比如 Fragment 类型的组件在生成 fiber 节点的时候会略过)

fiber 对象

react-reconciler 包是 react 应用的中枢,连接渲染器(react-dom)和调度中心(scheduler),同时自身也负责 fiber 树的构造

fiber.tag:表示 fiber 类型,根据 ReactElement 组件的 type 进行生成,在 react 内部定义了 25 种 tag

fiber.key:和 ReactElement 组件的 key 一致

fiber.return:指向父节点

fiber.child:指向第一个子节点

fiber.sibling:指向下一个兄弟节点

fiber.index:fiber 在兄弟节点中的索引,如果是单节点默认是 0

fiber.ref:指向 ReactElement 组件上设置的 ref(string 类型的 ref 除外,reconciler 阶段会将 string 类型的 ref 转换为一个 function 类型)

fiber.lanes:本 fiber 节点所属的优先级,创建 fiber 的时候设置

fiber.alternate:指向内存中另一个的 fiber,每个被更新过 fiber 节点在内存中都是成对出现(current 和 workInProgress)

UpdateQueue 与 UpdateQueue

属性解释
1.UpdateQueue:

  • baseState:表示此队列的基础 state

  • firstBaseUpdate:指向基础队列的队首

  • lastBaseUpdate:指向基础队列的队尾

  • shared:共享队列

  • effects:用于保存有 callback 回调函数 update 对象,在 commit 之后,会依次调用这里的回调函数

    2.Update:

  • lane:update 所属的优先级

  • tag:表示 update 种类,有 4 种,UpdateState,ReplaceState,ForceUpdate,CaptureUpdate

  • payload:载荷,update 对象真正需要跟新的数据,可以设置成一个回调函数或者对象

  • callback:回调函数,commit 完成之后调用

  • next:指向链表中的下一个,由于 UpdateQueue 是一个环形链表,最后一个 update.next 指向第一个 update 对象

UpdateQueue 是 fiber 对象的一个属性,所以不能脱离 fiber 存在,他们之间数据结构和应用关系如下

Hook 对象

Hook 用于 function 组件中,能够保持 function 组件的状态(与 class 组件中的 state 在性质上是相同的,都是为了保持组件的状态),常用 api 有:useState,useEffect,useCallback 等

属性解释

1.Hook:

  • memoizedState:内存状态,用于输出成最终的 fiber 树、
  • baseState:基础状态,当 Hook.queue 更新过后 baseState 也会更新
  • baseQueue:基础队列状态,在 reconciler 阶段会辅助状态合并
  • queue:指向一个 Update 队列
  • next:指向该 function 组件的下一个 Hook 对象,使得多个 Hook 之间也构成了一个链表

Hook.queue 和 Hook.baseQueue(即 UpdateQueue 和 Update)是为了保证 Hook 对象能够顺利更新,与上下文 fiber.updateQueue 中的 UpdateQueue 和 Update 是不一样的(且他们在不同的文件中)

Hook 与 fiber 的关系:

在 fiber 对象中有一个属性 fiber.memoizedState 指向 fiber 节点的内存状态,在 function 类型的组件中,fiber.memoizedState 就指向 Hook 队列(Hook 队列保持了 function 类型的组件状态)

所以 Hook 也不能脱离 fiber 而存在

Task 对象

scheduler 包中,没有为 task 对象定义 type,其定义是直接在 js 代码中

属性解释:

  • id:位移标识
  • callback:task 最核心的字段,指向 react-reconciler 包所提供的回调函数
  • prioritylevel:优先级
  • startTime:一个时间戳,代表 task 的开始时间(创建时间+演示时间)
  • expirationTime:过期时间
  • sortIndex:控制 task 在队列中的次序,值越小越靠前

注意:task 中没有 next 属性,他不是一个链表,其顺序是通过排序来实现的(小顶锥数组,始终保证数组中的第一个 task 对象的优先级最高)

reconciler

reconciler 执行流程

react-reconciler 包的主要作用,主要功能分为以下 4 个方面:

  1. 输入:暴露 api 函数(如:schedleUpdateOnfiber),供给其它包(如 react 包)调用
  2. 注册调度任务:与调度中心(schedler 包)交互,注册调度任务 task,等待任务回调
  3. 执行任务回调:在内存中构造出 fiber 树,同时与渲染器(react-com)交互,在内存中创建出与 fiber 对应的 DOM 节点
  4. 输出:与渲染器(react-dom)交互,渲染 DOM 节点

在 reconciler 执行过程中

1、Render(基于 task,可以被打断,可以被打断的前提是基于渲染 mode)

  • 初始化 fiber
  • 更新 fiber

2、commit

  • dom 变更之前
  • dom 变更之后
  • dom 更新之后

reconciler 启动过程

创建全局对象

1、ReactDOM(Blocking)Root 对象

  • 属于 react-dom 包,该对象暴露有 render,unmount 方法,通过调用该实例的 render 方法,可以引导 react 应用的启动

2、fiberRoot 对象

  • 属于 react-reconciler 包,作为 react-reconciler 在运行过程中的全局上下文,保存 fiber 构建过程所依赖的全局状态
  • 其它大部分实例变量用来存储 fiber 构建循环过程的各种状态 react 应用内部,可以根据这些实例变量的值,控制执行逻辑

3、HostRootFiber 对象

  • 属于 react-reconciler 包,这是 react 应用中的第一个 fiber 对象,是 fiber 树的根节点,节点的类型是 HostRoot

这 3 个对象是 react 体系得以运行的基本保障,一经创建大多数场景下不会再销毁(除非卸载整个 root.unmount())

这一过程是从 react-dom 包发起的,内部调用了 react-reconciler 包,

创建 ReactDOM Root 对象

  1. 调用 ReacDOM.createRoot 创建 ReactDOMRoot 实例
  2. 调用 ReacDOMRoot 实例的 render 方法
  3. 调用 createRootImpl 创建的 fiberRoot 对象,将其挂载到 this._internalRoot 上
  4. 原型上有 render 和 unmount 方法,且内部会调用 updateContainer 进行更新

创建 fiberRoot 对象

无论那种模式下,再 ReactDOM(Blocking)Root 的创建过程中,都会调用一个相同的函数 createRootImpl,查看后续的函数调用,最后会创建 fiberRoot 对象

创建 HostRootFiber 对象

再 createFiberRoot 中,创建了 react 应用的首个 fiber 对象,称为 HostRootFiber(fiber.tag=HostRoot)

fiber 树中所有节点的 mode 都会和 HostRootFiber.mode 一致(新建的 fiber 节点,其 mode 来源与父节点),所以 HostRootFiber.mode 很重要,决定了以后整个 fiber 树构建过程

可中断渲染

可中断渲染(render 可以中断,部分生命周期函数有可能执行多次)

UNSAFE_componentWillMount,UNSAFE_componentWillReceiverProps 只有在 HostRootFiber.mode===ConcurrentRoot 才会开启

如果使用的是 legacy,即通过 ReactDOM.render(,dom)这种方式启动时 HostRootFiber.mode=NoMode,这种情况下无论是首次 render 还是后续 update 都只会进入同步工作循环,reconciliation 没有机会中断,所以生命周期只会调用一次

优先级管理

React 内部对于优先级的管理,贯穿运作流程的 4 个阶段,根据功能的不同,可以分为 3 种类型

  1. fiber 优先级(LanePriority):位于 react-reconciler 包,也就是 Lane(车道模型)
  2. 调度优先级(SchedulerPriority):位于 scheduler 包
  3. 优先级等级(ReactPriorityLevel):位于 react-reconciler 包中的 SchedulerWithReactIntegration.js,负责上述 2 套优先级体系的转换

Lane(车道模型)

  1. Lane 类型被定义为二进制变量,利用了位掩码的特性,在频繁运算的时候占用内存少,计算速度快
  2. Lane 是对于 expirationTime 的重构,以前使用 expirationTime 表示的字段,都改为了 lane

总结:

  • 可以使用的比特位一共有 31 位
  • 共定义了 18 中车道(Lane/Lanes)变量,每一个变量占有一个或者多个比特位,分别定义 Lane 和 Lanes 类型
  • 每一种车道都有对应的优先级
  • 占有地位比特位的 Lane 变量对应的优先级越高

Lane 的位运算

程序中的所有数在计算机内存中都是以二进制的形式储存的,位运算就是直接对整数在内存中的二进制位进行操作

位运算在 react 中的运用

优先级区别和联系

  1. LanePriority 和 SchedulerPriority 从命名上看,它们代表的是优先级
  2. ReactPriorityLevel:从命名上看,它代表的是等级而不是优先级,它用于衡量 LanePriority 和 SchedulerPriority 的等级

React 调度原理

在 react 原型过程中,调度中心位于 scheduler 包,是整个 react 运行时的中枢

  • react 两大循环:任务调度循环是主角
  • reconciler 运行流程:分为 4 个阶段,其中第 2 个阶段注册调度任务串联了 scheduler 包和 react-reconciler 包,其实就是任务调度循环中的一个任务(task)
  • 优先级管理:其中 Schedulerpriority 控制任务调度循环中的循环顺序

调度实现

调度中心最核心的代码在 SchedulerHostConfig.default.js 中

内核

该 js 文件一共导出了 8 个函数,核心逻辑就在函数中

在不同的 js 执行环境中,这些函数的实现会有区别,下面基于普通浏览器对函数分析

1、调度相关:请求或取消调度

  • requestHostCallback
  • cancelHostCallback
  • requestHostTimeout
  • cancelHostTimeout

这 4 个函数目的就是请求执行(或取消)回调函数

主要是及时回调,延时回调在 17.2 之后基本没用

任务队列管理

1、在 Scheduler.js 中,维护了一个 taskQueue,任务队列管理就是围绕这个 taskQueue 展开

  • taskQueue 是一个小顶堆数组
  • 源码中除了 taskQueue 队列之外还有一个 timerQueue 队列,这个队列是预留给演示任务使用的,在[email protected]版本里面,算是一个保留功能,没有用到

2、创建任务

  • 在 unstable_scheduleCallback 函数中

3、消费任务

  • 在下一时间循环中,最终执行 flushwork

fiber 树构建

在 react 运行时,fiber 树构造位于 react-reconciler 包,reconciler 的 4 个阶段如下:

  1. 输入阶段:衔接 react-dom 包,承接 fiber 更新请求
  2. 注册调度任务:与调度中心 scheduler 包交互,注册调度任务 task,等待任务回调
  3. 执行任务回调:在内存中构造出 fiber 树和 dom 对象,也是 fiber 树构造的重点内容
  4. 输出:与渲染器 react-dom 交互,渲染 DOM 节点

fiber 树构造处于第三个阶段,可以通过不同的视角来理解 fiber 树构造在 react 运行时中所处的位置

  • 从 scheduler 调度中心的角度来看,它是任务队列 taskQueue 中的一个具体的任务回调(task.callback)
  • 从 react 工作循环的角度来看,它属于 fiber 树构造循环

根据 react 运行的内存状态,分为两种情况

  1. 初次创建:在 react 应用首次启动时,界面还没有渲染,此时并不会进入对比过程,相当于直接构造一棵全新的树
  2. 对比跟新:react 应用启动后,界面已经渲染,如果再次发生更新,创建新 fiber 之前需要和旧 fiber 进行对比,最后构造的 fiber 树有可能是全新的,也可能是部分更新

ReacElement,Fiber,DOM 三者关系

1、ReacElement 对象(type 定义在 shared 包中)

  • 所有采用 jsx 语法书写的节点,都会编译器转换,最终会以 React.createElement(…)的方式,创建出来一个与之对应的 ReactElement 对象

2、fiber 对象(type 类型的定义在 ReactInternaIType.js 中)

  • fiber 对象是通过 ReactElement 对象进行创建的,多个 fiber 对象构成了一颗 fiber 树,fiber 树是构造 DOM 树的数据模型,fiber 树的任何改动,最后都体现到 DOM 树

3、DOM 对象:文档对象模型

  • DOM 将文档解析为一个由节点和对象(包含属性和方法的对象)组成的结构集合,也就是常说的 DOM 树
  • JavaScript 可以访问和操作存储在 DOM 中的内容,也就是操作 DOM 对象,进而触发 UI 渲染

双缓冲技术(double buffering)

在全局变量中 workInProgress,还有不少 workInProgress 来命名的变量,workInProgress 的应用实际上就是 React 的双缓冲技术

在 react 的上下文中,这种机制主要通过 fiber 架构实现,具体体现在维护两颗 fiber 树上:当前 fiber 树(curren tree)和工作中的树(work-in-progress tree,简称 WIP tree)

  • 当前 fiber 树:当前正在屏幕上展示的用户界面结构,每个 fiber 节点对应着 UI 中的一个组件或者 DOM 元素
  • 工作中的 fiber 树:当 react 需要对 UI 更新时,不是直接进行修改当前树,而是创建一个新的 fiber 树(WIP tree)。在这个树中,react 会对即将应用的变化进行计算和规划,包括哪些部分需要更新、添加或删除

工作流程:

  1. 更新调度:当 react 接收到状态变更或者 props 变更,它会开始在 WIP tree 上执行 reconciliation(协调)过程,对比新旧状态,找出最小的 DOM 操作
  2. 并发与分片:react fiber 运行更新过程在多个任务之间中断和恢复,着意味着复杂的更新可以被拆分成小块(time slicing),提高了 UI 的响应性,这是双缓冲在任务调度层面的体现
  3. 一次性渲染:一旦 WIP tree 构建完成并且所有必要的计算和优化都已完成,react 会将这颗树一次性地替换掉当前树,这个瞬间替换的过程很快,因为它实际上是对 DOM 树的一次整体更新,而非逐步修改,从而减少了页面重排和重绘的次数。

react 的双缓冲技术通过分离计划(构建 WIP terr)和执行(替换 current tree)阶段,实现了高效的 UI 更新管理,提示了用户体验。

优先级

在整个 react-reconciler 包中,Lane 的应用可以分为 3 个方面

update 优先级(update.lane)

在 react 体系中,有 2 种情况会创建 update 对象:

  1. 引用初始化:在 react-reconciler 包中的 updateContainer 函数中
  2. 发起组件更新,假设在 class 组件中调用 setState

requestUpdateLane:

返回一个合适的 update 优先级

  • legacy 模式:返回 SyncLane
  • concurrent 模式:
    正常情况下,根据当前的调度优先级来生成一个 lane
    特殊情况下(处于 suspense 过程中),会优先选择 TransitionLanes 通道中的空闲通道(如果所有 TransitionLanes 通道都被占用,就取最高优先级)

最后通过 scheduleUpdateOnFiber(current,lane,eventTime)函数,把 update.lane 正式带入到输入阶段

渲染优先级(renderLanes)

这是一个全局概念,每一次 render 之前,首先要确定本次 render 的优先级

无论是 legacy 还是 Concurrent 模式,在正式 render 之前,都会调用 getNextLanes 获取一个优先级

getNextLanes 会根据 fiberRoot 对象上的属性(expiredLanes,suspendedLanes,pingedLanes 等),确定出当前最紧急的 lanes

此处返回的 lanes 会作为全局渲染的优先级,用于 fiber 树构造过程中,针对 fiber 对象,或 update,只要它们的优先级(如:fiber.lanes 和 update.lane)比渲染优先级低,都将会被忽略

fiber 优先级

在 fiber 对象的数据结构中,其中有 2 个属性与优先级相关:

  1. fiber.lanes:代表本节点的优先级
  2. fiber.childLanes:代表子节点的优先级从 FiberNode 的构造函数中可以看出,fiber.lanes 和 fiber.childLanes 的初始值都为 NoLanes,在 fiber 树构造过程中,使用全局的渲染优先级(renderLanes)和 fiber.lanes 判断 fiber 节点是否更新
    如果全局的渲染优先级 renderLanes 不包括 fiber.lanes,证明该 fiber 节点没有更新,可以复用
    如果不能复用,进入创建阶段

栈帧模型

每次 fiber 树的构造是一个独立的过程,需要独立的一组全局变量,在 React 内部把这一个独立的过程封装为一个栈帧 stack(简单来说就是每次构造都需要独立的空间)

在进行 fiber 树构造之前,如果不需要恢复上一次的构造进度,都会刷新栈帧

你可能感兴趣的:(react,面试,react.js,前端)