【从0实现React18】 (五) 初探react mount流程 完成核心递归流程

news2024/9/21 12:21:16

更新流程的目的:

  • 生成wip fiberNode树
  • 标记副作用flags

更新流程的步骤:

  • 递:beginWork
  • 归:completeWork

在 上一节 ,我们探讨了 React 应用在首次渲染或后续更新时的整体更新流程。在 Reconciler 工作流程中,beginWorkcompleteWork 两个方法起到了关键作用。beginWork 负责构建表示更新的 Fiber 树,而 completeWork 则将这个 Fiber 树映射到实际的 DOM 结构上。接下来,我们将深入实现这两个方法。

  1. 开发环境打印日志

首先,在开发环境下增加 DEV 标识,以便在开发环境中方便地打印日志:

pnpm i -D -w @rollup/plugin-replace

安装完成后,在 scripts/rollup/utils.js 中引入:

 // scripts/rollup/utils.js  
 
 // ...  
 
 import replace from  '@rollup/plugin-replace' ;  
 // ...   
 
 export  function  getBaseRollupPlugins ( { 
     alias = { __DEV__: true }, 
     typescript = {} 
 } = {} 
 ) {  
     return [ replace (alias), cjs (), ts (typescript)]; 
 } 

这样我们就可以在开发环境中打印日志了。

  1. 实现 beginWork

对于如下结构的reactElement:

<A>
    <B/>
<A/>

当进入A的beginWork时,通过对比B的current fiberNode 与B的reactElement,生成对应wip fiberNode


beginWork 函数在向下遍历阶段执行,根据 Fiber 节点的类型(HostRootHostComponentHostText)分发任务给不同的处理函数,处理具体节点类型的更新逻辑:

export const beginWork = (wip: FiberNode) => {
  switch (wip.tag) {
    case HostRoot:
      return updateHostRoot(wip)
    case HostComponent:
      return updateHostComponent(wip)
    case HostText:
      return null
    default:
      if (__DEV__) {
        console.warn('beginWork为实现的类型', wip)
      }
      break
  }
}
  1. HostRoot

    /** 处理根节点的更新,包括协调处理根节点的属性 以及子节点的更新逻辑 */
    function updateHostRoot(wip: FiberNode) {
      const baseState = wip.memoizedState
      const updateQueue = wip.updateQueue as UpdateQueue<Element>
      const pending = updateQueue.shared.pending
      updateQueue.shared.pending = null // 清空更新链表
    
      // 1.计算状态的最新值
      const { memoizedState } = processUpdateQueue(baseState, pending) // 计算待更新状态的最新值
      wip.memoizedState = memoizedState // 更新协调后的状态最新值
    
      // 2. 创造子fiberNode
      const nextChildren = wip.memoizedState // 获取 children 属性
      reconcileChildren(wip, nextChildren) // 处理根节点的子节点,可能会递归调用其他协调函数;
    
      // 返回经过协调后的新的子节点链表
      return wip.child
    }
    
    • 表示根节点,即应用的最顶层节点;
    • 调用 updateHostRoot 函数,处理根节点的更新,包括协调处理根节点的属性以及子节点的更新逻辑;
    • 调用 reconcileChildren 函数,处理根节点的子节点,可能会递归调用其他协调函数;
    • 返回 workInProgress.child 表示经过协调后的新的子节点链表;
  1. HostComponent

    function updateHostComponent(wip: FiberNode) {
      const nextProps = wip.pendingProps
      //  创造子fiberNode
      const nextChildren = nextProps.children // 获取Dom的children属性
      reconcileChildren(wip, nextChildren) // 处理原生 DOM 元素的子节点更新,可能会递归调用其他协调函数;
    
      return wip.child
    }
    
    • 表示原生 DOM 元素节点,例如 <div><span> 等;
    • 调用 updateHostComponent 函数,处理原生 DOM 元素节点的更新,负责协调处理属性和子节点的更新逻辑;
    • 调用 reconcileChildren 函数,处理原生 DOM 元素的子节点更新;
    • 返回 workInProgress.child 表示经过协调后的新的子节点链表;
  1. HostText

    • 表示文本节点,即 DOM 中的文本内容,例如 <p>123</p> 中的 123
    • 调用 updateHostText 函数,协调处理文本节点的内容更新;
    • 返回 null 表示已经是叶子节点,没有子节点了;

其中 reconcileChildren 函数的作用是,通过对比子节点的 current FiberNode 与 子节点的 ReactElement,来生成子节点对应的 workInProgress FiberNode。(current 是与视图中真实 UI 对应的 Fiber 树,workInProgress 是触发更新后正在 Reconciler 中计算的 Fiber 树。)

function reconcileChildren(wip: FiberNode, children?: ReactElementType) {
  const current = wip.alternate

  if (current !== null) {
    // update
    wip.child = reconcileChildFibers(wip, current?.child, children)
  } else {
    // mount
    wip.child = mountChildFibers(wip, null, children)
  }
}

reconcileChildren 函数中调用了 reconcileChildFibersmountChildFibers 两个函数,它们分别负责处理更新阶段和首次渲染阶段的子节点协调。

reconcileChildFibers:

  • reconcileChildFibers 函数作用于组件的更新阶段,即当组件已经存在于 DOM 中,需要进行更新时。
  • 主要任务是协调处理当前组件实例的子节点,对比之前的子节点(current)和新的子节点(workInProgress)之间的变化。
  • 根据子节点的类型和 key 进行对比,决定是复用、更新、插入还是删除子节点,最终形成新的子节点链表。

mountChildFibers:

  • mountChildFibers 函数作用于组件的首次渲染阶段,即当组件第一次被渲染到 DOM 中时。
  • 主要任务是协调处理首次渲染时组件实例的子节点。
  • 但此时是首次渲染,没有之前的子节点,所以主要是创建新的子节点链表。
 // packages/react-reconciler/src/childFiber.ts
/** 实现生成子节点fiber 以及标记Flags的过程 */

import { ReactElementType } from 'shared/ReactTypes'
import { FiberNode, createFiberFromElement } from './fiber'
import { REACT_ELEMENT_TYPE } from 'shared/ReactSymbols'
import { HostText } from './workTags'
import { Placement } from './fiberFlags'

function ChildrenReconciler(shouldTrackEffects: boolean) {
  /** 处理单个 Element 节点的情况
  对比 currentFiber 与 ReactElement
  生成 workInProgress FiberNode */
  function reconcileSingleElement(
    returnFiber: FiberNode,
    currentFiber: FiberNode | null,
    element: ReactElementType
  ) {
    // 根据element创建fiber
    const fiber = createFiberFromElement(element)
    fiber.return = returnFiber
    return fiber
  }

  /** 处理文本节点的情况
    对比 currentFiber 与 ReactElement
    生成 workInProgress FiberNode */
  function reconcileSingleTextNode(
    returnFiber: FiberNode,
    currentFiber: FiberNode | null,
    content: string | number
  ) {
    const fiber = new FiberNode(HostText, { content }, null)
    fiber.return = returnFiber
    return fiber
  }

  /** 为 Fiber 节点添加更新 flags */
  function placeSingleChild(fiber: FiberNode) {
    // 优化策略,首屏渲染且追踪副作用时,才添加更新 flags
    if (shouldTrackEffects && fiber.alternate === null) {
      fiber.flags |= Placement
    }
    return fiber
  }

  // 闭包,根据 shouldTrackSideEffects 返回不同 reconcileChildFibers 的实现
  return function reconcileChildFibers(
    returnFiber: FiberNode,
    currentFiber: FiberNode | null,
    newChild?: ReactElementType
  ) {
    // 判断当前fiber的类型
    // 1. 单个 Element 节点
    if (typeof newChild === 'object' && newChild !== null) {
      switch (newChild.$$typeof) {
        case REACT_ELEMENT_TYPE:
          return placeSingleChild(
            reconcileSingleElement(returnFiber, currentFiber, newChild)
          )
        default:
          if (__DEV__) {
            console.warn('未实现的reconcile类型', newChild)
          }
          break
      }
    }
    // TODO 2. 多个 Element 节点 ul > li*3

    // 3. HostText 节点
    if (typeof newChild === 'string' || typeof newChild === 'number') {
      return placeSingleChild(
        reconcileSingleTextNode(returnFiber, currentFiber, newChild)
      )
    }
    // 4. 其他
    if (__DEV__) {
      console.warn('未实现的reconcile类型', newChild)
    }
    return null
  }
}

/** 处理更新阶段的子节点协调,组件的更新阶段中,追踪副作用*/
export const reconcileChildFibers = ChildrenReconciler(true)
/** 处理首次渲染阶段的子节点协调,首屏渲染阶段中不追踪副作用,只对根节点执行一次 DOM 插入操作*/
export const mountChildFibers = ChildrenReconciler(false)
// packages/react-reconciler/src/fiber.ts
// ...

/** 根据element创建fiber并返回 */
export function createFiberFromElement(element: ReactElementType) {
  const { type, key, props } = element

  let fiberTag: WorkTag = FunctionComponent

  if (typeof type === 'string') {
    // <div/>  type: 'div'
    fiberTag = HostComponent
  } else if (typeof type !== 'function' && __DEV__) {
    console.warn('未定义的type类型', element)
  }
  // 创建 fiber 节点
  const fiber = new FiberNode(fiberTag, props, key)
  fiber.type = type

  return fiber
}

beginWork性能优化策略

考虑如下结构的reacetElement:

<div>
    <p>练习时长</p>
    <span>两年半</span>
</div>

理论上mount流程完毕后包含的flags:

  • 两年半 Placement
  • Span Placement
  • 练习时长 Placement
  • P Placement
  • Div Placement

相比执行5次Placement,我们可以构建好离屏DOM后,对div执行1次Placement操作

  1. 实现 completeWork

需要解决的问题:

  • 对于 Host 类型 fiberNode: 构建离屏Dom树
  • 标记 Update flag (TODO)

completeWork 函数在向上遍历阶段执行,根据 Fiber 节点的类型(HostRootHostComponentHostText 等)构建 DOM 节点,收集更新 flags,并根据更新 flags 执行不同的 DOM 操作:

  • HostComponent:

    • 表示原生 DOM 元素节点;
    • 构建 DOM 节点,并调用 appendAllChildren 函数将 DOM 插入到 DOM 树中;
    • 收集更新 flags,并根据更新 flags 执行不同的 DOM 操作,例如插入新节点、更新节点属性、删除节点等;
  • HostText:

    • 表示文本节点;
    • 构建 DOM 节点,并将 DOM 插入到 DOM 树中;
    • 收集更新 flags,根据 flags 的值,更新文本节点的内容;
  • HostRoot:

    • 表示根节点;
    • 会执行一些与根节点相关的最终操作,例如处理根节点的属性,确保整个应用更新完毕;
export const completeWork = (wip: FiberNode) => {
  const newProps = wip.pendingProps
  const current = wip.alternate
  switch (wip.tag) {
    case HostComponent:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // mount  构建离屏的 Dom 树
        // 1. 构建 Dom
        const instance = createInstance(wip.type, newProps)
        // 2. 将Dom插入到Dom树中
        appendAllChildren(instance, wip)
        wip.stateNode = instance
      }
      bubbleProperties(wip)
      return null
    case HostText:
      if (current !== null && wip.stateNode) {
        //update
      } else {
        // mount
        // 1. 构建 Dom
        const instance = createTextInstance(newProps.content)
        wip.stateNode = instance
      }
      bubbleProperties(wip)
      return null
    case HostRoot:
      bubbleProperties(wip)
      return null
    default:
      if (__DEV__) {
        console.warn('completeWork未实现的类型', wip)
      }
      break
  }
}

其中,appendAllChildren 函数负责递归地将组件的子节点添加到指定的 parent 中,它通过深度优先遍历 workInProgress 的子节点链表,处理每个子节点的类型。先处理当前节点的所有子节点,再处理兄弟节点。

如果它是原生 DOM 元素节点或文本节点,则将其添加到父节点中;如果是其他类型的组件节点并且有子节点,则递归处理其子节点。

 // packages/react-reconciler/src/completeWork.ts
// ...
function appendAllChildren(parent: FiberNode, wip: FiberNode) {
  let node = wip.child

  // 递归插入
  while (node !== null) {
    if (node?.tag === HostComponent || node?.tag === HostText) {
      appendInitialChild(parent, node?.stateNode)
    } else if (node.child !== null) {
      node.child.return = node
      node = node.child
      continue
    }

    // 终止条件
    if (node === wip) {
      return
    }

    // 子节点结束,开始处理兄弟节点
    while (node.sibling === null) {
      // 1.当前节点无兄弟节点
      if (node.return === null || node.return === wip) {
        return
      }
      node = node?.return
    }
    // 2.当前节点有兄弟节点
    node.sibling.return = node.return
    node = node.sibling
  }
}

completeWork 性能优化策略

flags分布在不同fiberNode中,如何快速他们?

  • 利用completeWork向上遍历(归)的流程,将子fiberNode的flags冒泡到父fiberNode

创建 bubbleProperties 函,负责在 completeWork 函数向上遍历的过程中,通过向上冒泡子节点的 flags,将所有更新 flags 收集到根节点。主要包含以下步骤:

  • 从当前需要冒泡属性的 Fiber 节点开始,检查是否有需要冒泡的属性。
  • 如果当前节点有需要冒泡的属性,将这些属性冒泡到父节点的 subtreeFlags 或其他适当的属性中。
  • 递归调用 bubbleProperties 函数,处理父节点,将属性继续冒泡到更上层的祖先节点,直至达到根节点。
 // packages/react-reconciler/src/completeWork.ts
// ...

/** 收集更新 flags,将子 FiberNode 的 flags 冒泡到父 FiberNode 上 */
function bubbleProperties(wip: FiberNode) {
  let subtreeFlags = NoFlags
  let child = wip.child

  while (child !== null) {
    subtreeFlags |= child.subtreeFlags
    subtreeFlags |= child.flags

    child.return = wip
    child = child.sibling
  }
  wip.subtreeFlags |= subtreeFlags
}
  1. 位运算简介

flags 是 React 中很重要的一环,具体作用是通过二进制在每个 Fiber 节点保存其本身与子节点的 flags。在保存与处理 flags 时,使用了一些二进制运算符,我们来复习一下:

1. | 运算

| 运算的两个位都为 0 时,结果才为 0:

  • 1 | 1 = 1
  • 1 | 0 = 1
  • 0 | 0 = 0

React 利用了 | 运算符的特性来存储 flags,如:

const NoFlags = /*            */ 0b0000000;
const PerformedWork = /*      */ 0b0000001;
const Placement = /*          */ 0b0000010;
const Update = /*             */ 0b0000100;
const ChildDeletion = /*      */ 0b0001000;

const flags = Placement | Update; //此时 flags = 0b0000110

2. & 运算

& 运算的两个位都为 1 时,结果才为 1:

  • 1 & 1 = 1
  • 1 & 0 = 0
  • 0 & 0 = 0

React 中会用一个 flags & 某一个 flag,来判断 flags 中是否包含某一个 flag,如:

const flags = Placement | Update; //此时 flags = 0b0000110

Boolean(flags & Placement); // true, 说明 flags 中包含 Placement
Boolean(flags & ChildDeletion); // false, 说明 flags 中不包含 ChildDeletion

3. ~ 运算

运算符会把每一位取反,0 变 1,1 变 0:

  • ~1 = 0
  • ~0 = 1

在 React 中,~ 运算符同样是常用操作,如:

let flags = Placement | Update; //此时 flags = 0b0000110

flags &= ~Placement; //此时 flags = 0b0000100

通过 ~ 运算符与 & 运算符的结合,从 flags 中删除了 Placement 这个 flag。


至此,我们就实现了 React 协调阶段中的 beginWorkcompleteWork 函数,生成了一棵表示更新的 Fiber 树,并收集了树中节点的更新 flags,下一节我们将根据这些 flags 执行对应的 DOM 操作。

借鉴链接: https://juejin.cn/post/7347911786802511924

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1870094.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

硬盘空间告急?监控服务器容量,钉钉及时提醒!

在日常的服务器维护中&#xff0c;硬盘容量的监控是非常重要的。如果硬盘容量超过某个阈值&#xff0c;可能会导致服务器无法正常运行&#xff0c;影响业务的正常运作。为了避免这种情况&#xff0c;我们可以编写一个Shell脚本&#xff0c;定期检查硬盘容量&#xff0c;当超过设…

springboot + Vue前后端项目(第二十记)

项目实战第二十记 写在前面1. 高德地图官网2. 开发文档3. 集成高德地图3.1 在public文件夹下创建config.js3.2 index.html&#xff08;在项目启动文件中引入外部的js&#xff09;3.3 点标记&#xff08;用点标记当前位置&#xff09;3.4 信息窗体&#xff08;点击当前位置&…

如何预防和处理他人盗用IP地址?

IP地址的定义及作用 解释 IP 地址在互联网中的作用。它是唯一标识网络设备的数字地址&#xff0c;类似于物理世界中的邮政地址。 1、IP地址盗窃的定义 解释一下什么是IP地址盗用&#xff0c;即非法使用他人的IP地址或者伪造IP地址的行为&#xff0c;这种行为可能引发法律和安…

DLMS/COSEM协议—(Green-Book)Gateway protocol

DLMS/COSEM协议 — Gateway protocol 10.10 Gateway protocol &#xff08;网关协议&#xff09;10.10.1 概述10.10.2 网关协议 &#xff08;The gateway protocol&#xff09;10.10.3 HES在WAN/NN中作为发起者&#xff08;拉取操作&#xff09;10.10.4 LAN中的终端设备作为发起…

数据库物理结构设计-定义数据库模式结构(概念模式、用户外模式、内模式)、定义数据库、物理结构设计策略

一、引言 如何基于具体的DBMS产品&#xff0c;为数据库逻辑结构设计的结果&#xff0c;即关系数据库模式&#xff0c;制定适合应用要求的物理结构 1、在设计数据库物理结构前&#xff0c;数据库设计人员首先 要充分了解所用的DBMS产品的功能、性能和特点&#xff0c;包括提供…

【最新综述】基于伪标签的半监督语义分割

Semi-Supervised Semantic Segmentation Based on Pseudo-Labels: A Survey 摘要&#xff1a; 语义分割是计算机视觉领域的一个重要而热门的研究领域&#xff0c;其重点是根据图像中像素的语义对其进行分类。然而&#xff0c;有监督的深度学习需要大量数据来训练模型&#xff…

Asm动态生成类和get and set方法

asm在解析文件的时候是按照特定顺序进行分析的&#xff0c;首先是visit方法&#xff0c;做类相关的解析&#xff0c;然后是注解&#xff0c;然后是属性&#xff0c;最后才是方法&#xff0c;属性是在所有方法分析前面进行&#xff0c;也就是只有当class文件中的所有属性都遍历完…

【Android11】开机启动日志捕捉服务

一、前言 制作这个功能的原因是客户想要自动的记录日志中的报错和警告到设备的内存卡里面。虽然开发者模式中有一个“bug report” 会在/data/user_de/0/com.android.shell/files/bugreports/目录下生成一个zip包记录了日志。但是客户觉得这个日志很难获取到他们需要的信息&am…

无需劳师动众,让石油化工DCS集散控制系统轻松实现无线传输!

石油化工中,为了保证较高的可靠性和安全性,大量使用的是DCS集散控制系统。与FCS现场总线的“现场采集,转换为数字信号来集中传输”不同,DCS系统为了避免由于线缆断裂或者节点问题导致整个控制系统失灵,采用“分散传输,集中采集”的方式,即每个传感器通过4-20mA的模拟量通…

el-upload 上传图片及回显照片和预览图片,文件流和http线上链接格式操作

<div v-for"(info, index) in zsjzqwhxqList.helicopterTourInfoList" :key"info.id" >编辑上传图片// oss返回线上地址http链接格式&#xff1a;<el-form-itemlabel"巡视结果照片":label-width"formLabelWidth"><el…

【MySQL】 -- 用户管理

1. 权限 如果我们只能使用root用户&#xff0c;这样存在安全隐患。这时&#xff0c;就需要使用MySQL的用户管理。创建出非root用户&#xff0c;限制其权限。 权限这个概念拿出来就是用来限制非root用户的。这样从技术手段上保证了数据的安全性和完整性&#xff0c;防止有人删库…

LeetCode11. 盛最多水的容器题解

LeetCode11. 盛最多水的容器题解 题目链接&#xff1a; https://leetcode.cn/problems/container-with-most-water 示例 思路 暴力解法 定住一个柱子不动&#xff0c;然后用其他柱子与其围住面积&#xff0c;取最大值。 代码如下&#xff1a; public int maxArea1(int[]…

麒麟系统安装Redis

一、背景 如前文&#xff08;《麒麟系统安装MySQL》&#xff09;所述。 二、下载Redis源码 官方未提供麒麟系统的Redis软件&#xff0c;须下载源码编译。 下载地址&#xff1a;https://redis.io/downloads 6.2.14版本源码下载地址&#xff1a;https://download.redis.io/re…

ADI-DSP|在指定内存写入数据

一、LDF文件设置内存空间 user_data_test { TYPE(BW RAM) START(0x00380010) END(0x0039bfff) WIDTH(8) }//usr data dxe_user_data_bw BW{INPUT_SECTION_ALIGN(4)INPUT_SECTIONS( $OBJS_LIBS(user_data) )} > user_data_test 二、在C文件中设置数据 /************…

2.x86游戏实战-跨进程读取血量

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 本次游戏没法给 内容参考于&#xff1a;微尘网络安全 接下来会写C/C代码&#xff0c;C/C代码不是很难&#xff0c;然后为了快速掌握逆向这个技能&#xff0c;我…

List常用操作比for循环更优雅的写法

private String name; //姓名 private Integer age; //年龄 private Integer departId; //所属部门id } List list new ArrayList<>(); 复制代码 简单遍历 使用lamada表达式之前&#xff0c;如果需要遍历list时&#xff0c;一般使用增强for循环&#xff0c;代码如…

uboot基本使用网络命令和从服务器端下载linux内核启动

网络命令ip地址设置: setenv gmac_debug 0; setenv mdio_intf rgmii; setenv bootdelay 1; setenv ethaddr 00:xxxx:81:70; // mac地址 setenv ipaddr xxx; //开发板 IP 地址 setenv netmask 255.255.255.0; setenv gatewayip xxx.1; setenv serverip xxxx; //服…

RP2040 开发,用 Arduino 通过 ADC 获取电压测量数据

这两天测试了一下如何通过 RP2040 的内置 ADC 获取一个待测量的电压数据&#xff0c;RP2040 内置了4路ADC&#xff0c;分辨率是12bit&#xff0c;也就是说&#xff0c;可以获取4096阶的变化量&#xff0c;但第4个 ADC 已经用于测量芯片的内部温度&#xff0c;所以实际能用的仅有…

YOLO模型评价指标

在模型训练完成之后&#xff0c;需要对模型的优劣作出评估&#xff0c;YOLO系列算法的评价指标包括&#xff1a; 1. 准确率&#xff08;Precision&#xff09;&#xff1a;指模型预测为正样本中实际为正样本的比例。 &#x1d447;&#x1d443;、&#x1d439;&#x1d443;、…

计算机方向国际学术会议推荐

*华中师范大学伍伦贡联合研究院与南京大学联合主办 第三届人工智能、物联网和云计算技术国际会议&#xff08;AIoTC 2024&#xff09; 大会官网&#xff1a;www.icaiotc.net 时间地点&#xff1a;2024年9月13-15日&#xff0c;中国武汉 收录检索&#xff1a;EI Compendex&a…