chenglong

深入理解Render阶段Fiber树的初始化与更新

notion image

一、前言

为什么有这篇文章?当时有人问我下面这个点击button,网页应该变成什么样? 注意他们的key是相同的
notion image
我去看了7km老师的博客[1] 收集到了答案
答案和你想象的一样吗??不一样就继续往下看看呗!!!结尾有答案

二、前置概念

react框架可以用来表示,输入状态 —> 吐出ui。

react架构是什么?

可以分为如下三层:
  1. scheduler(调度器):用来分发优先级更高的任务。
  1. render阶段(协调器):找出哪些节点发生了变化,并且给相应的fiber打上标签。
  1. commit阶段(渲染器):将打好标签的节点渲染到视图上。遍历effectList执行对应的dom操作或部分生命周期
流程图 (36).jpg
  1. 输入: 将每一次更新(如: 新增, 删除, 修改节点之后)视为一次更新需求(目的是要更新DOM节点).
  1. 注册调度任务: react-reconciler收到更新需求之后, 并不会立即构造fiber树, 而是去调度中心scheduler注册一个新任务task, 即把更新需求转换成一个task.
  1. 执行调度任务(输出): 调度中心scheduler通过任务调度循环来执行task
    1. fiber构造循环是task的实现环节之一, 循环完成之后会构造出最新的 fiber 树.
    2. commitRoot是task的实现环节之二, 把最新的 fiber 树最终渲染到页面上, task完成.
主干逻辑就是输入到输出这一条链路, 为了更好的性能(如批量更新, 可中断渲染等功能), react在输入到输出的链路上做了很多优化策略, 任务调度循环和fiber构造循环相互配合就可以实现可中断渲染.
流程图 (39).jpg

ReactElement, Fiber, DOM 三者的关系

上面我们大概提及了一下react的架构和更新的粗略流程,考虑到本文的重点是Render阶段发生了啥,接下来上重量级嘉宾JSX,ReactElement, Fiber, DOM。以下面这个jsx代码为例,讲解三者的关系
createElement源码
所有采用JSX语法书写的节点, 都会被编译器转换, 最终会以React.createElement(...)的方式, 创建出来一个与之对应的ReactElement对象.
这也是为什么在每个使用JSX的JS文件中,你必须显式的声明 import React from 'react';(17版本后不需要)否则在运行时该模块内就会报未定义变量 React的错误。

ReactElement数据结构和内存结构(结合上面jsx示例代码)

数据结构

内存结构

流程图 (21).jpg

Fiber 对象数据结构

数据结构

内存结构

流程图 (22).jpg

ReactElement, Fiber, DOM 三者的关系

流程图 (23).jpg

React的启动过程发生了啥

接下来介绍的都是当前稳定版legacy 模式
在没有进入render阶段(react-reconciler包)之前,reactElement(<App/>)和 DOM 对象div#root之间没有关联。
流程图 (33).jpg
在react初始化的时候,会创建三个全局对象,在三个对象创建完毕的时候,react初始化完毕。
  1. ReactDOMRoot对象
    1. 属于react-dom包,该对象暴露有render,unmount方法, 通过调用该实例的ReactDOM.render方法, 可以引导 react 应用的启动.
  1. fiberRoot对象
    1. 属于react-reconciler包,在运行过程中的全局上下文, 保存 fiber 构建过程中所依赖的全局状态,
    2. 其大部分实例变量用来存储fiber构造循环过程的各种状态,react 应用内部, 可以根据这些实例变量的值, 控制执行逻辑。
  1. HostRootFiber对象
    1. 属于react-reconciler包,这是 react 应用中的第一个 Fiber 对象, 是 Fiber 树的根节点, 节点的类型是HostRoot.
这 3 个对象是 react 体系得以运行的基本保障, 除非卸载整个应用,否则不会再销毁
流程图 (34).jpg
此刻内存中各个对象的引用情况表示出来,此时reactElement(<App/>)还是独立在外的, 还没有和目前创建的 3 个全局对象关联起来
流程图 (35).jpg
到此为止, react内部经过一系列运转, 完成了初始化。

三、render阶段发生了啥

以下所有示例按照下面的代码 请注意

双缓冲fiber技术

在上文我们梳理了ReactElement, Fiber, DOM三者的关系, fiber树的构造过程, 就是把ReactElement转换成fiber树的过程. 但是在这个过程中, 内存里会同时存在 2 棵fiber树:
  • 其一: 代表当前界面的fiber树(已经被展示出来, 挂载到fiberRoot.current上). 如果是初次构造(初始化渲染), 页面还没有渲染, 此时界面对应的 fiber 树为空(fiberRoot.current = null).
  • 其二: 正在构造的fiber树(即将展示出来, 挂载到HostRootFiber.alternate上, 正在构造的节点称为workInProgress). 当构造完成之后, 重新渲染页面, 最后切换fiberRoot.current = workInProgress, 使得fiberRoot.current重新指向代表当前界面的fiber树.

React入口初始化内存情况

在进入react-reconciler包之前,也就是还没render时, 内存状态图如下,和上面启动过程的图对应:
流程图 (24).jpg

fiber 树构造方式

  1. 初次创建: 在React应用首次启动时, 界面还没有渲染, 此时并不会进入对比过程, 相当于直接构造一棵全新的树.
  1. 对比更新: React应用启动后, 界面已经渲染. 如果再次发生更新, 创建新fiber之前需要和旧fiber进行对比. 最后构造的 fiber 树有可能是全新的, 也可能是部分更新的.
在深度优先遍历中, 每个fiber节点都会经历 2 个阶段:
  1. 探寻阶段 beginWork
  1. 回溯阶段 completeWork

beginWork探寻阶段发生了什么源码地址[2]

  1. 创建节点:根据 ReactElement对象创建所有的fiber节点, 最终构造出fiber树形结构(设置returnsibling指针)
  1. 给节点打标签:设置fiber.flags(二进制形式变量, 用来标记 fiber节点 的增,删,改状态, 等待completeWork阶段处理)
  1. 设置真实DOM的局部状态:设置fiber.stateNode局部状态(如Class类型节点: fiber.stateNode=new Class())

completeWork回溯阶段发生了什么源码地址[3]

  1. 调用completeWork
    1. fiber节点(tag=HostComponent, HostText)创建 DOM 实例, 设置fiber.stateNode局部状态(如tag=HostComponent, HostText节点: fiber.stateNode 指向这个 DOM 实例).
    2. 为 DOM 节点设置属性, 绑定事件(合成事件原理).
    3. 设置fiber.flags标记
  1. 把当前 fiber 对象的副作用队列(firstEffectlastEffect)添加到父节点的副作用队列之后, 更新父节点的firstEffectlastEffect指针.
  1. 识别beginWork阶段设置的fiber.flags, 判断当前 fiber 是否有副作用(增,删,改), 如果有, 需要将当前 fiber 加入到父节点的effects队列, 等待commit阶段处理.

初次创建

这有一个动画 具体如果想看流程图可以点击[4]
初始化fiber.gif
下面标注了生成时期的 beginWorkcompleteWork 执行过程
动画演示了初次创建fiber树的全部过程, 跟踪了创建过程中内存引用的变化情况. fiber树构造循环负责构造新的fiber树, 构造过程中同时标记fiber.flags, 最终把所有被标记的fiber节点收集到一个副作用队列中, 这个副作用队列被挂载到根节点上(HostRootFiber.alternate.firstEffect). 此时的fiber树和与之对应的DOM节点都还在内存当中, 等待commitRoot阶段进行渲染
流程图 (32).jpg

对比更新的时候发生了什么

1.优化原则
  1. 只对同级节点进行对比,如果DOM节点跨层级移动,则react不会复用
      • 我们可以从同级的节点数量将Diff分为两类:
  1. 不同类型的元素会产出不同的结构,会销毁老的结构,创建新的结构
  1. 可以通过key标示移动的元素
  1. 类型一致的节点才有继续diff的必要性
  • 单节点对应演示,可以去浏览器的Elements>Properties查看
单节点.jpg
  • 多节点对应演示
image.png

diff算法介绍

1.单节点
  1. 如果是新增节点, 直接新建 fiber, 没有多余的逻辑
  1. 如果是对比更新
      • 如果keytype都相同,则复用
      • 否则新建
单节点的逻辑比较简明, 源码[5]
2.多节点
  1. 多节点一般会存在两轮遍历,第一轮寻找公共序列,第二轮遍历剩余非公共序列
  1. 第一次循环 源码[6]
      • key不同导致不可复用,立即跳出整个遍历,第一轮遍历结束。
      • key相同type不同导致不可复用,会将oldFiber标记为DELETION,并继续遍历
    1. 如果newChildren遍历完(即i === newChildren.length - 1)或者oldFiber遍历完(即oldFiber.sibling === null),跳出遍历,第一轮遍历结束。
    2. let i = 0,遍历newChildren,将newChildren[i]oldFiber比较,判断DOM节点是否可复用。
    3. 如果可复用,i++,继续比较newChildren[i]oldFiber.sibling,可以复用则继续遍历。
    4. 如果不可复用,分两种情况:
image.png
image.png
  1. 第二次循环: 遍历剩余非公共序列, 优先复用 oldFiber 序列中的节点。
      • 如果newChildrenoldFiber同时遍历完,diff结束
      • 如果 newChildren没遍历完,oldFiber遍历完,意味着没有可以复用的节点了,遍历剩下的newChildren为生成的workInProgress fiber依次标记Placement
      • 如果newChildren遍历完,oldFiber没遍历完,意味着有节点被删除了,需要遍历剩下的oldFiber,依次标记Deletion
      • 如果newChildrenoldFiber都没遍历完 (重点)源码[7]
image.png
image.png
下面动画展示了fiber的对比更新过程 每一张流程图链接[8]
fiber对比更新.gif
流程图 (28).jpg

四、检验学习成果

为什么网页会变成那个样子?
流程图 (29).jpg
流程图 (30).jpg
流程图 (38).jpg
image.png

五、参考

7km:7kms.github.io/react-illus…[9]
冴羽:juejin.cn/post/716098…[10]
卡颂:react.iamkasong.com/preparation…[11]
xiaochen1024.com/article\_ite…[12]
如果有错误的话欢迎大家帮忙指正嗷!!!强烈推荐7km的图解react! 谢谢大家~~~~
关于本文
作者:抱枕同学
https://juejin.cn/post/7202085514400038969

最后

欢迎关注「三分钟学前端」号内回复:
「网络」,自动获取三分钟学前端网络篇小书(90+页)「JS」,自动获取三分钟学前端 JS 篇小书(120+页)「算法」,自动获取 github 2.9k+ 的前端算法小书「面试」,自动获取 github 23.2k+ 的前端面试小书「简历」,自动获取程序员系列的 120 套模版》》面试官也在看的前端面试资料《《
“在看和转发”就是最大的支持

Copyright © 2024 chenglong

logo