【从0实现React18】 (五) 初探react mount流程 完成核心递归流程
更新流程的目的:
- 生成wip fiberNode树
- 标记副作用flags
更新流程的步骤:
- 递:beginWork
- 归:completeWork
在 上一节 ,我们探讨了 React 应用在首次渲染或后续更新时的整体更新流程。在 Reconciler
工作流程中,beginWork
和 completeWork
两个方法起到了关键作用。beginWork
负责构建表示更新的 Fiber 树,而 completeWork
则将这个 Fiber 树映射到实际的 DOM 结构上。接下来,我们将深入实现这两个方法。
首先,在开发环境下增加 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)]; }
这样我们就可以在开发环境中打印日志了。
对于如下结构的reactElement:
<A><B/>
<A/>
当进入A的beginWork
时,通过对比B的current fiberNode
与B的reactElement
,生成对应wip fiberNode
beginWork
函数在向下遍历阶段执行,根据 Fiber 节点的类型(HostRoot
、HostComponent
、HostText
)分发任务给不同的处理函数,处理具体节点类型的更新逻辑:
export const beginWork = (wip: FiberNode) => {switch (wip.tag) {case HostRoot:return updateHostRoot(wip)case HostComponent:return updateHostComponent(wip)case HostText:return nulldefault:if (__DEV__) {console.warn('beginWork为实现的类型', wip)}break}
}
-
HostRoot
/** 处理根节点的更新,包括协调处理根节点的属性 以及子节点的更新逻辑 */ function updateHostRoot(wip: FiberNode) {const baseState = wip.memoizedStateconst updateQueue = wip.updateQueue as UpdateQueue<Element>const pending = updateQueue.shared.pendingupdateQueue.shared.pending = null // 清空更新链表// 1.计算状态的最新值const { memoizedState } = processUpdateQueue(baseState, pending) // 计算待更新状态的最新值wip.memoizedState = memoizedState // 更新协调后的状态最新值// 2. 创造子fiberNodeconst nextChildren = wip.memoizedState // 获取 children 属性reconcileChildren(wip, nextChildren) // 处理根节点的子节点,可能会递归调用其他协调函数;// 返回经过协调后的新的子节点链表return wip.child }
- 表示根节点,即应用的最顶层节点;
- 调用
updateHostRoot
函数,处理根节点的更新,包括协调处理根节点的属性以及子节点的更新逻辑; - 调用
reconcileChildren
函数,处理根节点的子节点,可能会递归调用其他协调函数; - 返回
workInProgress.child
表示经过协调后的新的子节点链表;
-
HostComponent
function updateHostComponent(wip: FiberNode) {const nextProps = wip.pendingProps// 创造子fiberNodeconst nextChildren = nextProps.children // 获取Dom的children属性reconcileChildren(wip, nextChildren) // 处理原生 DOM 元素的子节点更新,可能会递归调用其他协调函数;return wip.child }
- 表示原生 DOM 元素节点,例如
<div>
、<span>
等; - 调用
updateHostComponent
函数,处理原生 DOM 元素节点的更新,负责协调处理属性和子节点的更新逻辑; - 调用
reconcileChildren
函数,处理原生 DOM 元素的子节点更新; - 返回
workInProgress.child
表示经过协调后的新的子节点链表;
- 表示原生 DOM 元素节点,例如
-
HostText
- 表示文本节点,即 DOM 中的文本内容,例如
<p>123</p>
中的123
; - 调用
updateHostText
函数,协调处理文本节点的内容更新; - 返回
null
表示已经是叶子节点,没有子节点了;
- 表示文本节点,即 DOM 中的文本内容,例如
其中 reconcileChildren
函数的作用是,通过对比子节点的 current FiberNode
与 子节点的 ReactElement
,来生成子节点对应的 workInProgress FiberNode
。(current
是与视图中真实 UI 对应的 Fiber 树,workInProgress
是触发更新后正在 Reconciler 中计算的 Fiber 树。)
function reconcileChildren(wip: FiberNode, children?: ReactElementType) {const current = wip.alternateif (current !== null) {// updatewip.child = reconcileChildFibers(wip, current?.child, children)} else {// mountwip.child = mountChildFibers(wip, null, children)}
}
reconcileChildren
函数中调用了 reconcileChildFibers
和 mountChildFibers
两个函数,它们分别负责处理更新阶段和首次渲染阶段的子节点协调。
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创建fiberconst fiber = createFiberFromElement(element)fiber.return = returnFiberreturn fiber}/** 处理文本节点的情况对比 currentFiber 与 ReactElement生成 workInProgress FiberNode */function reconcileSingleTextNode(returnFiber: FiberNode,currentFiber: FiberNode | null,content: string | number) {const fiber = new FiberNode(HostText, { content }, null)fiber.return = returnFiberreturn fiber}/** 为 Fiber 节点添加更新 flags */function placeSingleChild(fiber: FiberNode) {// 优化策略,首屏渲染且追踪副作用时,才添加更新 flagsif (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 } = elementlet fiberTag: WorkTag = FunctionComponentif (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 = typereturn 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操作
需要解决的问题:
- 对于 Host 类型 fiberNode: 构建离屏Dom树
- 标记 Update flag (TODO)
completeWork
函数在向上遍历阶段执行,根据 Fiber 节点的类型(HostRoot
、HostComponent
、HostText
等)构建 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.pendingPropsconst current = wip.alternateswitch (wip.tag) {case HostComponent:if (current !== null && wip.stateNode) {//update} else {// mount 构建离屏的 Dom 树// 1. 构建 Domconst instance = createInstance(wip.type, newProps)// 2. 将Dom插入到Dom树中appendAllChildren(instance, wip)wip.stateNode = instance}bubbleProperties(wip)return nullcase HostText:if (current !== null && wip.stateNode) {//update} else {// mount// 1. 构建 Domconst instance = createTextInstance(newProps.content)wip.stateNode = instance}bubbleProperties(wip)return nullcase HostRoot:bubbleProperties(wip)return nulldefault: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 = nodenode = node.childcontinue}// 终止条件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.returnnode = 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 = NoFlagslet child = wip.childwhile (child !== null) {subtreeFlags |= child.subtreeFlagssubtreeFlags |= child.flagschild.return = wipchild = child.sibling}wip.subtreeFlags |= subtreeFlags
}
- 位运算简介
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 = 0b0000110Boolean(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 = 0b0000110flags &= ~Placement; //此时 flags = 0b0000100
通过 ~
运算符与 &
运算符的结合,从 flags 中删除了 Placement
这个 flag。
至此,我们就实现了 React 协调阶段中的 beginWork
和 completeWork
函数,生成了一棵表示更新的 Fiber 树,并收集了树中节点的更新 flags,下一节我们将根据这些 flags 执行对应的 DOM 操作。
借鉴链接: https://juejin.cn/post/7347911786802511924
相关文章:

【从0实现React18】 (五) 初探react mount流程 完成核心递归流程
更新流程的目的: 生成wip fiberNode树标记副作用flags 更新流程的步骤: 递:beginWork归:completeWork 在 上一节 ,我们探讨了 React 应用在首次渲染或后续更新时的整体更新流程。在 Reconciler 工作流程中ÿ…...

0-30 VDC 稳压电源,电流控制 0.002-3 A
怎么运行的 首先,有一个次级绕组额定值为 24 V/3 A 的降压电源变压器,连接在电路输入点的引脚 1 和 2 上。(电源输出的质量将直接影响与变压器的质量成正比)。变压器次级绕组的交流电压经四个二极管D1-D4组成的电桥整流。桥输出端…...

HTML5+CSS3+JS小实例:图片九宫格
实例:图片九宫格 技术栈:HTML+CSS+JS 效果: 源码: 【HTML】 <!DOCTYPE html> <html lang="zh-CN"> <head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1…...
湘潭大学软件工程数据库总结
文章目录 前言试卷结构给学弟学妹的一些参考自己的一些总结 前言 自己可能很早很早之前就准备复习了,但是感觉还是没有学到要点,主要还是没啥紧迫的压力,我们是三月份开学,那时候实验室有朋友挺认真开始学习数据库了,…...
Codeforces Testing Round 1 B. Right Triangles 题解 组合数学
Right Triangles 题目描述 You are given a n m nm nm field consisting only of periods (‘.’) and asterisks (‘*’). Your task is to count all right triangles with two sides parallel to the square sides, whose vertices are in the centers of ‘*’-cells. …...

怎样将word默认Microsoft Office,而不是WPS
设置——>应用——>默认应用——>选择"word"——>将doc和docx都选择Microsoft Word即可...

C语言之进程的学习2
Env环境变量(操作系统的全局变量)...

web使用cordova打包Andriod
一.安装Gradel 1.下载地址 Gradle Distributions 2.配置环境 3.测试是否安装成功 在cmd gradle -v 二.创建vite项目 npm init vitelatest npm install vite build 三.创建cordova项目 1.全局安装cordova npm install -g cordova 2. 创建项目 cordova create cordova-app c…...
内卷情况下,工程师也应该了解的项目管理
简介:大家好,我是程序员枫哥,🌟一线互联网的IT民工、📝资深面试官、🌹Java跳槽网创始人。拥有多年一线研发经验,曾就职过科大讯飞、美团网、平安等公司。在上海有自己小伙伴组建的副业团队&…...

【解锁未来:深入了解机器学习的核心技术与实际应用】
解锁未来:深入了解机器学习的核心技术与实际应用 💎1.引言💎1.1 什么是机器学习? 💎2 机器学习的分类💎3 常用的机器学习算法💎3.1 线性回归(Linear Regression)…...

1-3.文本数据建模流程范例
文章最前: 我是Octopus,这个名字来源于我的中文名–章鱼;我热爱编程、热爱算法、热爱开源。所有源码在我的个人github ;这博客是记录我学习的点点滴滴,如果您对 Python、Java、AI、算法有兴趣,可以关注我的…...

【FFmpeg】avformat_alloc_output_context2函数
【FFmpeg】avformat_alloc_output_context2函数 1.avformat_alloc_output_context21.1 初始化AVFormatContext(avformat_alloc_context)1.2 格式猜测(av_guess_format)1.2.1 遍历可用的fmt(av_muxer_iterate࿰…...
Flask 缓存和信号
Flask-Caching Flask-Caching 是 Flask 的一个扩展,它为 Flask 应用提供了缓存支持。缓存是一种优化技术,可以存储那些费时且不经常改变的运算结果,从而加快应用的响应速度。 一、初始化配置 安装 Flask-Caching 扩展: pip3 i…...

基于weixin小程序农场驿站系统的设计
管理员账户功能包括:系统首页,个人中心,农场资讯管理,用户管理,卖家管理,用户分享管理,分享类型管理,商品信息管理,商品类型管理 开发系统:Windows 架构模式…...
JAVA将List转成Tree树形结构数据和深度优先遍历
引言: 在日常开发中,我们经常会遇到需要将数据库中返回的数据转成树形结构的数据返回,或者需要对转为树结构后的数据绑定层级关系再返回,比如需要统计当前节点下有多少个节点等,因此我们需要封装一个ListToTree的工具类…...
设计模式——开闭、单一职责及里氏替换原则
设计原则是指导软件设计和开发的一系列原则,它们帮助开发者创建出易于维护、扩展和理解的代码。以下是你提到的几个关键设计原则的简要说明: 开闭原则(Open/Closed Principle, OCP): 开闭原则由Bertrand Meyer提出&am…...

代码随想录算法训练营第59天:动态[1]
代码随想录算法训练营第59天:动态 两个字符串的删除操作 力扣题目链接(opens new window) 给定两个单词 word1 和 word2,找到使得 word1 和 word2 相同所需的最小步数,每步可以删除任意一个字符串中的一个字符。 示例: 输入: …...

jvm性能监控常用工具
在java的/bin目录下有许多java自带的工具。 我们常用的有 基础工具 jar:创建和管理jar文件 java:java运行工具,用于运行class文件或jar文件 javac:java的编译器 javadoc:java的API文档生成工具 性能监控和故障处理 jps jstat…...

ISP IC/FPGA设计-第一部分-SC130GS摄像头分析-IIC通信(1)
1.摄像头模组 SC130GS通过一个引脚(SPI_I2C_MODE)选择使用IIC或SPI配置接口,通过查看摄像头模组的原理图,可知是使用IIC接口; 通过手册可知IIC设备地址通过一个引脚控制,查看摄像头模组的原理图ÿ…...
HTTP协议头中X-Forwarded-For是能做什么?
X-Forwarded-For和相关几个头部的理解 $remote_addr 是nginx与客户端进行TCP连接过程中,获得的客户端真实地址. Remote Address 无法伪造,因为建立 TCP 连接需要三次握手,如果伪造了源 IP,无法建立 TCP 连接,更不会有后…...
【Unity】R3 CSharp 响应式编程 - 使用篇(二)
一、通用的事件监听用法 using System;using R3;using UnityEngine;namespace Aladdin.Standard.Observable.Common{public class CommonObservable : MonoBehaviour{// 默认会调用1次public SerializableReactiveProperty<int> serializableReactiveProperty;…...

无人机避障——感知部分(Ubuntu 20.04 复现Vins Fusion跑数据集)胎教级教程
硬件环境:NVIDIA Jeston Orin nx 系统:Ubuntu 20.04 任务:跑通 EuRoC MAV Dataset 数据集 展示结果: 编译Vins Fusion 创建工作空间vins_ws # 创建目录结构 mkdir -p ~/vins_ws/srccd ~/vins_ws/src# 初始化工作空间…...
stm32——UART和USART
串口通信协议UART和USART 1. UART与USART协议详解 特性UART (Universal Asynchronous Receiver/Transmitter)USART (Universal Synchronous Asynchronous Receiver/Transmitter)全称通用异步收发器通用同步/异步收发器同步/异步异步:不共享时钟,数据通过…...

Android端口转发
如上图所示,有一个Android设备,Android设备里面有主板,主板上有网络接口和Wi-Fi,网络接口通过网线连接了一个网络摄像头,这就跟电脑一样,电脑即可以通过网线接入一个网络,也可以同时用Wi-Fi接入…...
网页端 VUE+C#/FastAPI获取客户端IP和hostname
1 IP可以获取,但是发现获取到的是服务端的IP,如何解决呢。 如果采用nginx反向代理,那么可以在conf/nginx.conf文件中配置 location /WebApi/ { proxy_pass http://localhost:5000/; #这个/会替换location种的WebApi路径 #关键,加…...

LangChain工具集成实战:构建智能问答系统完整指南
导读:在人工智能快速发展的今天,如何构建一个既能理解自然语言又能调用外部工具的智能问答系统,成为许多开发者面临的核心挑战。本文将为您提供一套完整的解决方案,从LangChain内置工具包的基础架构到复杂系统的工程实践。 文章深…...
分库分表的取舍
文章目录 大数据量下采用**水平分表**的缺点**1. 跨表查询复杂性与性能下降****2. 数据分布不均衡****3. 分布式事务与一致性问题****4. 扩展性受限****5. 查询条件限制与索引管理复杂****6. 数据迁移与维护成本高****7. 业务逻辑复杂度增加****总结** shardingJdbc分片策略**1…...

紫光同创FPGA系列实现Aurora 8b/10b协议
特性 1.兼容XILINX aurora IP核 2.支持X1、X2、X4、X8模式(根据硬件条件选择模式) 3.支持FRAMING和STREAMING 用户接口 4.自动初始化和维护链路状态 5.支持热插拔 6.支持扰码、解扰 7.支持流量控制 8.支持crc用户数据 9.支持全双工或者半双工模式 10.最…...
嵌入式面试高频(5)!!!C++语言(嵌入式八股文,嵌入式面经)
一、C有几种传值方式之间的区别 一、值传递(Pass by Value) 机制:创建参数的副本,函数内操作不影响原始数据语法:void func(int x)特点: 数据安全:原始数据不受影响性能开销:需要复…...

多模态大语言模型arxiv论文略读(109)
Math-PUMA: Progressive Upward Multimodal Alignment to Enhance Mathematical Reasoning ➡️ 论文标题:Math-PUMA: Progressive Upward Multimodal Alignment to Enhance Mathematical Reasoning ➡️ 论文作者:Wenwen Zhuang, Xin Huang, Xiantao Z…...