【从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 = 11 | 0 = 10 | 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 = 11 & 0 = 00 & 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 连接,更不会有后…...
.Net框架,除了EF还有很多很多......
文章目录 1. 引言2. Dapper2.1 概述与设计原理2.2 核心功能与代码示例基本查询多映射查询存储过程调用 2.3 性能优化原理2.4 适用场景 3. NHibernate3.1 概述与架构设计3.2 映射配置示例Fluent映射XML映射 3.3 查询示例HQL查询Criteria APILINQ提供程序 3.4 高级特性3.5 适用场…...
多场景 OkHttpClient 管理器 - Android 网络通信解决方案
下面是一个完整的 Android 实现,展示如何创建和管理多个 OkHttpClient 实例,分别用于长连接、普通 HTTP 请求和文件下载场景。 <?xml version"1.0" encoding"utf-8"?> <LinearLayout xmlns:android"http://schemas…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
大语言模型如何处理长文本?常用文本分割技术详解
为什么需要文本分割? 引言:为什么需要文本分割?一、基础文本分割方法1. 按段落分割(Paragraph Splitting)2. 按句子分割(Sentence Splitting)二、高级文本分割策略3. 重叠分割(Sliding Window)4. 递归分割(Recursive Splitting)三、生产级工具推荐5. 使用LangChain的…...
【CSS position 属性】static、relative、fixed、absolute 、sticky详细介绍,多层嵌套定位示例
文章目录 ★ position 的五种类型及基本用法 ★ 一、position 属性概述 二、position 的五种类型详解(初学者版) 1. static(默认值) 2. relative(相对定位) 3. absolute(绝对定位) 4. fixed(固定定位) 5. sticky(粘性定位) 三、定位元素的层级关系(z-i…...
Nuxt.js 中的路由配置详解
Nuxt.js 通过其内置的路由系统简化了应用的路由配置,使得开发者可以轻松地管理页面导航和 URL 结构。路由配置主要涉及页面组件的组织、动态路由的设置以及路由元信息的配置。 自动路由生成 Nuxt.js 会根据 pages 目录下的文件结构自动生成路由配置。每个文件都会对…...
sqlserver 根据指定字符 解析拼接字符串
DECLARE LotNo NVARCHAR(50)A,B,C DECLARE xml XML ( SELECT <x> REPLACE(LotNo, ,, </x><x>) </x> ) DECLARE ErrorCode NVARCHAR(50) -- 提取 XML 中的值 SELECT value x.value(., VARCHAR(MAX))…...
PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
html css js网页制作成品——HTML+CSS榴莲商城网页设计(4页)附源码
目录 一、👨🎓网站题目 二、✍️网站描述 三、📚网站介绍 四、🌐网站效果 五、🪓 代码实现 🧱HTML 六、🥇 如何让学习不再盲目 七、🎁更多干货 一、👨…...
管理学院权限管理系统开发总结
文章目录 🎓 管理学院权限管理系统开发总结 - 现代化Web应用实践之路📝 项目概述🏗️ 技术架构设计后端技术栈前端技术栈 💡 核心功能特性1. 用户管理模块2. 权限管理系统3. 统计报表功能4. 用户体验优化 🗄️ 数据库设…...
