Wsh's blog Wsh's blog
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)

wsh

热爱前端的程序媛
首页
  • 基础知识
  • ArkUI
  • UIAbility
  • 组件通信方式
  • 前端缓存
  • React
  • typescript
  • javascript
  • flutter
  • node
  • webpack
web3D😉
宝库📰
  • 分类
  • 标签
  • 归档
龙哥的大🐂之路 (opens new window)
GitHub (opens new window)
  • react基础

  • react更新特性

  • react进阶

    • react 架构演变
      • react render阶段
      • react commit阶段
      • react diff
      • react 状态更新
      • react hook
      • Concurrent Mode
    • react
    • react进阶
    2022-04-21
    目录

    react 架构演变

    # 01. 旧架构(react15)

    React15架构可以分为两层:

    • Reconciler(stack Reconciler 协调器)—— 负责找出变化的组件
    • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

    Reconciler:

    每当有更新发生时,Reconciler会做如下工作:

    • 调用函数组件、或class组件的render方法,将返回的JSX转化为虚拟DOM
    • 将虚拟DOM和上次更新时的虚拟DOM对比(递归处理虚拟DOM的)
    • 通过对比找出本次更新中变化的虚拟DOM
    • 通知Renderer将变化的虚拟DOM渲染到页面上

    架构图如下所示:

    缺陷出现在 stack Reconciler 递归更新,数据放在递归的调用栈中,一旦开始,无法终止,直到遍历完整棵树,才能将主线程释放。 如果主线程有用户操作或动画渲染操作,就必须等到主线程释放,才能被响应,无法快速响应,即失帧。

    # 02. 新架构(React16)

    React16架构可以分为三层:

    • Scheduler(调度器)—— 调度任务的优先级,高优任务优先进入Reconciler
    • Reconciler(fiber Reconciler协调器,基于fiber节点实现的)—— 负责找出变化的组件
    • Renderer(渲染器)—— 负责将变化的组件渲染到页面上

    相对比15。新增了Scheduler。 我们以浏览器是否有剩余时间作为任务中断的标准,那么我们需要一种机制,当浏览器有剩余时间时通知我们。 requestIdleCallback (opens new window)在浏览器空闲时期被调用。由于requestIdleCallback兼容性问题和触发频率问题,react没有采用。

    基于上述原因,React实现了功能更完备的requestIdleCallback polyfill,就是Scheduler (opens new window),除了在空闲时触发回调外,Scheduler还提供多种调度优先级供任务设置。

    Reconciler:

    更新工作从递归变成了可以中断的循环过程。每次循环都会调用shouldYield判断当前是否有剩余时间。

    /** @noinline */
    function workLoopConcurrent() {
      // Perform work until Scheduler asks us to yield
      while (workInProgress !== null && !shouldYield()) {
        workInProgress = performUnitOfWork(workInProgress);
      }
    }
    

    Reconciler与Renderer不再是交替工作,当Scheduler将任务交给Reconciler后,Reconciler会为变化的虚拟DOM打上代表增/删/更新的标记,类似这样

    整个Scheduler与Reconciler的工作都在内存中进行。只有当所有组件都完成Reconciler的工作,才会统一交给Renderer。

    架构图如下所示:

    # 03. fiber架构的心智模型

    神马是心智模型?

    官方简单理解就是:它是人【主观】认识事物的方法和习惯,是隐藏在你一切行为方式、思考方式背后的那些形式和规律。

    # 04. fiber架构的实现原理

    • fiber的结构

    react/packages/react-reconciler/src/ReactFiber.new.js

    function FiberNode(
      tag: WorkTag,
      pendingProps: mixed,
      key: null | string,
      mode: TypeOfMode,
    ){
      // 作为静态数据结构的属性
      this.tag = tag; // 组件类型,
      this.key = key;
      this.elementType = null;
      this.type = null;
      this.stateNode = null; //真实dom节点
    
      // 作为连接其他Fiber节点形成Fiber树
      this.return = null;
      this.child = null;
      this.sibling = null;
      this.index = 0; // 对于同级节点,代表他们插入的位置索引
    
      this.ref = null;
       
      // 作为动态的工作单元的属性
      this.pendingProps = pendingProps;
      this.memoizedProps = null;
      this.updateQueue = null;
      this.memoizedState = null;
      this.dependencies = null;
    
      this.mode = mode;
    
      this.effectTag = NoEffect;
      this.subtreeTag = NoSubtreeEffect;
      this.deletions = null;
      this.nextEffect = null;
    
      this.firstEffect = null;
      this.lastEffect = null;
      
      // 调度优先级相关
      this.lanes = NoLanes;
      this.childLanes = NoLanes;
    
      // 指向该Fiber在另一次更新时对应的Fiber
      this.alternate = null;  // fiber 架构的工作方式
      ...
    }
    
    • 作为架构来说

    每个fiber 节点有个对应的React Element,多个fiber 节点依赖下面三个属性连接:

    this.return = null;  // 指向父级Fiber节点
    
    this.child = null; // 指向子Fiber节点
    
    this.sibling = null; // 指向右边第一个兄弟Fiber节点
    

    举例说明:

    function App() {
      return (
        <div>
          wsh
          <span>age: 18</span>
        </div>
      )
    }
    

    对应的fiber树如下:

    • 作为静态数据结构:

    作为静态的数据结构,保存了组件相关信息

    this.tag = tag; // Fiber对应组件的类型 Function/Class/Host...
    
    this.key = key; // key属性
    
    this.elementType = null; // 大部分情况同type,某些情况不同,比如FunctionComponent使用React.memo包裹
    
    this.type = null; // 对于 FunctionComponent,指函数本身,对于ClassComponent,指class,对于HostComponent,指DOM节点tagName
    
    this.stateNode = null; // Fiber对应的真实DOM节点
    
    • 作为动态的工作单元:

    作为动态的工作单元,Fiber中下面参数保存了本次更新相关的信息

    // 保存本次更新造成的状态变化相关信息
    this.pendingProps = pendingProps;
    this.menoizedProps = null;
    this.updateQueue = null;
    this.memoizedState = null;
    this.dependencies = null;
    
    this.mode = mode;
    
    // 保存本次更新会造成的DOM操作
    this.effectTag = NoEffect;
    this.nextEffect = null;
    
    this.firstEffect = null;
    this.lastEffect = null;
    

    保存调度优先级字段在如下字段:

    // 调度优先级相关
    this.lanes = NoLanes;
    this.childLanes = NoLanes;
    

    # expirationTime/ lanes 优先级对比PR (opens new window):

    • Lanes 模型与 Expiration Times 模型相比有两个主要优点

      • Lanes将任务优先级的概念(任务 A 的优先级是否高于任务 B)与任务批处理(任务A是这组任务的一部分吗?)解耦
      • Lanes 可以用单一的 32 位数据类型表示许多不同的任务线程。

    在旧模型中,为了决定是否在正在处理的批处理中包含给定的工作单元,我们将比较它们的相对优先级:

    const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;
    

    这是因为react施加了一个约束,即除非还包括更高优先级的任务,否则不允许完成较低优先级的任务。给定优先级 A > B > C,如果不处理 A,就无法处理B;如果不处理完成 B 和 A,你也不能处理 C。 这个约束是在Suspense出现之前设计的,这个在当时是有意义的(具体什么意义,我就不深究了, 😁)。当我们的所有工作都受 CPU 限制时,除了按优先级之外,沒有太多理由按任何顺序处理任务。但是,当我们引入 IO 密集型任务(即 Suspense)时,我们可能会遇到较高优先级 IO 密集型任务阻止较低优先级 CPU 密集型任务完成的情况。

    Expiration Times 的一个类似缺陷限制了我们如何表达一组多个优先级。

    就内存或计算而言,使用 Set 对象是不切实际的。我们正在处理的存在性检查非常普遍,因此它们需要快速并使用尽可能少的内存。 作为妥协,我们通常会做的是维持一系列优先级。

    const isTaskIncludedInBatch = taskPriority <= highestPriorityInRange && taskPriority >= lowestPriorityInRange;
    

    旧模型将 优先级和 批处理这两个概念耦合到一个单一的数据类型中。 我们表达其中一个的能力是有限的,除非用影响另一个的术语。 在新模型中,我们将这两个概念解耦。 相反,任务组不是用相对数字表示,而是用位掩码表示:

    const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
    
    • 表示任务的位掩码类型称为 Lane
    • 表示批次的位掩码类型称为 Lanes

    在更具体的 React 术语中,由 setState 调度的更新对象包含一个通道字段,一个启用单个位的位掩码。 这将替换旧模型中的 update.expirationTime 字段。 另一方面,一个fiber 不仅仅与单个更新相关联,而是可能与多个更新相关联。 所以它有一个 lanes 字段,一个启用了零个或多个位的位掩码(旧模型中的 fiber.expirationTime); 和一个 childLanes 字段 (fiber.childExpirationTime)。启用零个或多个位(旧模型中的 fiber.expirationTime)和一个 childLanes 字段 (fiber.childExpirationTime)。

    Lanes是一种不透明的类型。 只能在 ReactFiberLane 模块内执行直接位掩码操作。 在其他地方,必须从该模块导入一个辅助函数。 这是一个折衷,但我认为最终是值得的,因为处理Lanes可能非常微妙,并且将所有逻辑放在一起将使我们更容易调整我们的启发式算法,而无需每次进行大量重构(像这样)。

    常见的 Expiration Time 字段,转化为 Lanes

    renderExpirationtime -> renderLanes
    update.expirationTime -> update.lane
    fiber.expirationTime -> fiber.lanes
    fiber.childExpirationTime -> fiber.childLanes
    root.firstPendingTime and root.lastPendingTime -> fiber.pendingLanes
    

    # 05. fiber架构的工作原理

    # 什么是双缓存?

    通俗来说,当我们用canvas绘制动画,每一帧绘制前都会调用ctx.clearRect清除上一帧的画面。如果当前帧画面计算量比较大,导致清除上一帧画面到绘制当前帧画面之间有较长间隙,就会出现白屏。为了解决这个问题,我们可以在内存中绘制当前帧动画,绘制完毕后直接用当前帧替换上一帧画面,由于省去了两帧替换间的计算时间,不会出现从白屏到出现画面的闪烁情况。

    这种在内存中构建并直接替换的技术叫做双缓存。

    React使用“双缓存”来完成Fiber树的构建与替换——对应着DOM树的创建与更新。

    # 双缓存Fiber树

    在React中最多会同时存在两棵Fiber树。

    • 当前屏幕上显示内容对应的Fiber树称为current Fiber>树
    • 正在内存中构建的Fiber树称为workInProgress Fiber树

    current Fiber树中的Fiber节点被称为current fiber,workInProgress Fiber树中的Fiber节点被称为workInProgress fiber,他们通过alternate属性连接。

    currentFiber.alternate === workInProgressFiber;
    workInProgressFiber.alternate === currentFiber;
    

    React应用的根节点fiber Node通过使current指针在不同Fiber树的rootFiber间切换来完成current Fiber树指向的切换。

    切换时机:即当workInProgress Fiber树构建完成交给Renderer渲染在页面上后,应用根节点的current指针指向workInProgress Fiber树,此时workInProgress Fiber树就变为current Fiber树。

    每次状态更新都会产生新的workInProgress Fiber树,通过current与workInProgress的替换,完成DOM更新。

    function App() {
      const [num, add] = useState(0);
      return (
        <p onClick={() => add(num + 1)}>{num}</p>
      )
    }
    
    ReactDOM.render(<App/>, document.getElementById('root'));
    

    首次渲染时:

    1. fiberRootNode的current会指向当前页面上已渲染内容对应Fiber树,即current Fiber树。
    2. 接下来进入render阶段,根据组件返回的JSX在内存中依次创建Fiber节点并连接在一起构建Fiber树,被称为workInProgress Fiber树。(下图中右侧为内存中构建的树,左侧为页面显示的树)
    3. 图中右侧已构建完的workInProgress Fiber树在commit阶段渲染到页面。 此时DOM更新为右侧树对应的样子。fiberRootNode的current指针指向workInProgress Fiber树使其变为current Fiber 树

    更新时:

    1. 接下来我们点击p节点触发状态改变,这会开启一次新的render阶段并构建一棵新的workInProgress Fiber 树
    2. workInProgress Fiber 树在render阶段完成构建后进入commit阶段渲染到页面上。渲染完毕后,workInProgress Fiber 树变为current Fiber 树。

    # jsx 与fiber 关系

    jsx是一种描述当前组件内容的数据结构,不包含schedule,reconclie,render所需的相关信息

    在新建时,Reconciler根据JSX描述的组件内容生成组件对应的Fiber节点。 再更新时,Reconclier 将jsx 数据与Fibler 节点保存的数据对比,生成对应的workInProgress fiber。

    #react
    react18 更新特性
    react render阶段

    ← react18 更新特性 react render阶段→

    最近更新
    01
    组件通信方式
    01-07
    02
    UIAbility
    01-07
    03
    ATKTS
    01-06
    更多文章>
    Theme by Vdoing | Copyright © 2022-2025 Wsh | MIT License
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式