React源码分析(二)渲染机制
准备工作
为了方便讲解,假设我们有下面这样一段代码:
function App(){const [count, setCount] = useState(0)useEffect(() => {setCount(1)}, [])const handleClick = () => setCount(count => count++)return (<div>勇敢牛牛, <span>不怕困难</span><span onClick={handleClick}>{count}</span></div>)
}ReactDom.render(<App />, document.querySelector('#root'))
在React项目中,这种jsx语法首先会被编译成:
React.createElement("App", null)
or
jsx("App", null)
这里不详说编译方法,感兴趣的可以参考:
babel在线编译
新的jsx转换
jsx语法转换后,会通过creatElement
或jsx
的api转换为React element
作为ReactDom.render()
的第一个参数进行渲染。
在上一篇文章Fiber
中,我们提到过一个React项目会有一个fiberRoot
和一个或多个rootFiber
。fiberRoot
是一个项目的根节点。我们在开始真正的渲染前会先基于root
DOM创建fiberRoot
,且fiberRoot.current = rootFiber
,这里的rootFiber
就是current
fiber树的根节点。
if (!root) {// Initial mountroot = container._reactRootContainer = legacyCreateRootFromDOMContainer(container, forceHydrate);fiberRoot = root._internalRoot;
}
在创建好fiberRoot
和rootFiber
后,我们还不知道接下来要做什么,因为它们和我们的<App />
函数组件没有一点关联。这时React开始创建update
,并将ReactDom.render()
的第一个参数,也就是基于<App />
创建的React element
赋给update
。
var update = {eventTime: eventTime,lane: lane,tag: UpdateState,payload: null,callback: element,next: null};
有了这个update
,还需要将它加入到更新队列中,等待后续进行更新。在这里有必要讲下这个队列的创建流程,这个创建操作在React有多次应用。
var sharedQueue = updateQueue.shared;var pending = sharedQueue.pending;if (pending === null) { // mount时只有一个update,直接闭环update.next = update;} else { // update时,将最新的update的next指向上一次的update, 上一次的update的next又指向最新的update形成闭环update.next = pending.next;pending.next = update;}// pending指向最新的update, 这样我们遍历update链表时, pending.next会指向第一个插入的update。sharedQueue.pending = update;
我将上面的代码进行了一下抽象,更新队列是一个环形链表结构,每次向链表结尾添加一个update
时,指针都会指向这个update
,并且这个update.next
会指向第一个更新:
上一篇文章也讲过,React最多会同时拥有两个fiber
树,一个是current
fiber树,另一个是workInProgress
fiber树。current
fiber树的根节点在上面已经创建,下面会通过拷贝fiberRoot.current
的形式创建workInProgress
fiber树的根节点。
到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成fiber
树和dom
树,并最终渲染到页面中。
render阶段
这个阶段并不是指把代码渲染到页面上,而是基于我们的代码画出对应的fiber
树和dom
树。
workloopSync
function workLoopSync() {while (workInProgress !== null) {performUnitOfWork(workInProgress);}
}
在这个循环里,会不断根据workInProgress找到对应的child作为下次循环的workInProgress,直到遍历到叶子节点,即深度优先遍历。在performUnitOfWork
会执行下面的beginWork
。
beginWork
简单描述下beginWork
的工作,就是生成fiber
树。
基于workInProgress
的根节点生成<App />
的fiber
节点并将这个节点作为根节点的child
,然后基于<App />
的fiber
节点生成<div />
的fiber
节点并作为<App />
的fiber
节点的child
,如此循环直到最下面的牛牛
文本。
注意, 在上面流程图中,updateFunctionComponent
会执行一个renderWithHooks
函数,这个函数里面会执行App()
这个函数组件,在这里会初始化函数组件里所有的hooks
,也就是上面实例代码的useState()
。
当遍历到牛牛
文本时,它的下面已经没有了child
,这时beginWork
的工作就暂时告一段落,为什么说是暂时,是因为在completeWork
时,如果遍历的fiber
节点有sibling
会再次走到beginWork
。相关参考视频讲解:进入学习
completeWork
当遍历到牛牛
文本后,会进入这个completeWork
。
在这里,我们再简单描述下completeWork
的工作, 就是生成dom
树。
基于fiber
节点生成对应的dom
节点,并且将这个dom
节点作为父节点,将之前生成的dom
节点插入到当前创建的dom
节点。并会基于在beginWork
生成的不完全的workInProgress
fiber树向上查找,直到fiberRoot
。在这个向上的过程中,会去判断是否有sibling
,如果有会再次走beginWork
,没有就继续向上。这样到了根节点,一个完整的dom
树就生成了。
额外提一下,在completeWork
中有这样一段代码
if (flags > PerformedWork) {if (returnFiber.lastEffect !== null) {returnFiber.lastEffect.nextEffect = completedWork;} else {returnFiber.firstEffect = completedWork;}returnFiber.lastEffect = completedWork;
}
解释一下, flags > PerformedWork
代表当前这个fiber
节点是有副作用的,需要将这个fiber
节点加入到父级fiber
的effectList
链表中。
commit阶段
这个阶段的主要工作是处理副作用。所谓副作用就是不确定操作,比如:插入,替换,删除DOM,还有useEffect()
hook的回调函数都会被作为副作用。
commitWork
准备工作
在commitWork
前,会将在workloopSync
中生成的workInProgress
fiber树赋值给fiberRoot
的finishedWork
属性。
var finishedWork = root.current.alternate; // workInProgress fiber树
root.finishedWork = finishedWork; // 这里的root是fiberRoot
root.finishedLanes = lanes;
commitRoot(root);
在上面我们提到,如果一个fiber
节点有副作用会被记录到父级fiber
的lastEffect
的nextEffect
。
在下面代码中,如果fiber
树有副作用,会将rootFiber.firstEffect
节点作为第一个副作用firstEffect
,并且将effectList
形成闭环。
var firstEffect;
// 判断当前rootFiber树是否有副作用
if (finishedWork.flags > PerformedWork) {// 下面代码的目的还是为了将这个effectList链表形成闭环if (finishedWork.lastEffect !== null) {finishedWork.lastEffect.nextEffect = finishedWork;firstEffect = finishedWork.firstEffect;} else {firstEffect = finishedWork;}
} else {
// 这个rootFiber树没有副作用
firstEffect = finishedWork.firstEffect;
}
mutation之前
简单描述mutation之前阶段的工作:
- 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑;
- 调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里;
- 调度useEffect(异步);
在mutation之前的阶段,遍历effectList
链表,执行commitBeforeMutationEffects
方法。
do { // mutation之前invokeGuardedCallback(null, commitBeforeMutationEffects, null);} while (nextEffect !== null);
我们进到commitBeforeMutationEffects
方法,我将代码简化一下:
function commitBeforeMutationEffects() {while (nextEffect !== null) {var current = nextEffect.alternate;// 处理DOM节点渲染/删除后的 autoFocus、blur 逻辑;if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null){...}var flags = nextEffect.flags;// 调用getSnapshotBeforeUpdate,fiberRoot和ClassComponent会走这里if ((flags & Snapshot) !== NoFlags) {...}// 调度useEffect(异步)if ((flags & Passive) !== NoFlags) {// rootDoesHavePassiveEffects变量表示当前是否有副作用if (!rootDoesHavePassiveEffects) {rootDoesHavePassiveEffects = true;// 创建任务并加入任务队列,会在layout阶段之后触发scheduleCallback(NormalPriority$1, function () {flushPassiveEffects();return null;});}}// 继续遍历下一个effectnextEffect = nextEffect.nextEffect;}
}
按照我们示例代码,我们重点关注第三件事,调度useEffect(注意,这里是调度,并不会马上执行)。
scheduleCallback
主要工作是创建一个task
:
var newTask = {id: taskIdCounter++,callback: callback, //上面代码传入的回调函数priorityLevel: priorityLevel,startTime: startTime,expirationTime: expirationTime,sortIndex: -1
};
它里面有个逻辑会判断startTime
和currentTime
, 如果startTime > currentTime
,会把这个任务加入到定时任务队列timerQueue
,反之会加入任务队列taskQueue
,并task.sortIndex = expirationTime
。
mutation
简单描述mutation阶段的工作就是负责dom渲染。
区分fiber.flags
,进行不同的操作,比如:重置文本,重置ref,插入,替换,删除dom节点。
和mutation之前阶段一样,也是遍历effectList
链表,执行commitMutationEffects
方法。
do { // mutation dom渲染invokeGuardedCallback(null, commitMutationEffects, null, root, renderPriorityLevel);} while (nextEffect !== null);
看下commitMutationEffects
的主要工作:
function commitMutationEffects(root, renderPriorityLevel) {// TODO: Should probably move the bulk of this function to commitWork.while (nextEffect !== null) { // 遍历EffectListsetCurrentFiber(nextEffect);// 根据flags分别处理var flags = nextEffect.flags;// 根据 ContentReset flags重置文字节点if (flags & ContentReset) {...}// 更新refif (flags & Ref) {...}var primaryFlags = flags & (Placement | Update | Deletion | Hydrating);switch (primaryFlags) {case Placement: // 插入dom{...}case PlacementAndUpdate: //插入dom并更新dom{// PlacementcommitPlacement(nextEffect);nextEffect.flags &= ~Placement; // Updatevar _current = nextEffect.alternate;commitWork(_current, nextEffect);break;}case Hydrating: //SSR{...}case HydratingAndUpdate: // SSR{...}case Update: // 更新dom{...}case Deletion: // 删除dom{...}}resetCurrentFiber();nextEffect = nextEffect.nextEffect;}
}
按照我们的示例代码,这里会走PlacementAndUpdate
,首先是commitPlacement(nextEffect)
方法,在一串判断后,最后会把我们生成的dom
树插入到root
DOM节点中。
function appendChildToContainer(container, child) {var parentNode;if (container.nodeType === COMMENT_NODE) {parentNode = container.parentNode;parentNode.insertBefore(child, container);} else {parentNode = container;parentNode.appendChild(child); // 直接将整个dom作为子节点插入到root中}
}
到这里,代码终于真正的渲染到了页面上。下面的commitWork
方法是执行和useLayoutEffect()
有关的东西,这里不做重点,后面文章安排,我们只要知道这里是执行上一次更新的effect unmount
。
fiber树切换
在讲layout
阶段之前,先来看下这行代码
root.current = finishedWork // 将`workInProgress`fiber树变成`current`树
这行代码在mutation和layout阶段之间。在mutation阶段, 此时的current
fiber树还是指向更新前的fiber
树, 这样在生命周期钩子内获取的DOM就是更新前的, 类似于componentDidMount
和compentDidUpdate
的钩子是在layout
阶段执行的,这样就能获取到更新后的DOM进行操作。
layout
简单描述layout阶段的工作:
- 调用生命周期或hooks相关操作
- 赋值ref
和mutation之前阶段一样,也是遍历effectList
链表,执行commitLayoutEffects
方法。
do { // 调用生命周期和hook相关操作, 赋值refinvokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
} while (nextEffect !== null);
来看下commitLayoutEffects
方法:
function commitLayoutEffects(root, committedLanes) {while (nextEffect !== null) {setCurrentFiber(nextEffect);var flags = nextEffect.flags;// 调用生命周期或钩子函数if (flags & (Update | Callback)) {var current = nextEffect.alternate;commitLifeCycles(root, current, nextEffect);}{// 获取dom实例,更新refif (flags & Ref) {commitAttachRef(nextEffect);}}resetCurrentFiber();nextEffect = nextEffect.nextEffect;}
}
提一下,useLayoutEffect()
的回调会在commitLifeCycles
方法中执行,而useEffect()
的回调会在commitLifeCycles
中的schedulePassiveEffects
方法进行调度。从这里就可以看出useLayoutEffect()
和useEffect()
的区别:
useLayoutEffect
的上次更新销毁函数在mutation
阶段销毁,本次更新回调函数是在dom渲染后的layout
阶段同步执行;useEffect
在mutation之前
阶段会创建调度任务,在layout
阶段会将销毁函数和回调函数加入到pendingPassiveHookEffectsUnmount
和pendingPassiveHookEffectsMount
队列中,最终它的上次更新销毁函数和本次更新回调函数都是在layout
阶段后异步执行; 可以明确一点,他们的更新都不会阻塞dom渲染。
layout之后
还记得在mutation之前
阶段的这几行代码吗?
// 创建任务并加入任务队列,会在layout阶段之后触发
scheduleCallback(NormalPriority$1, function () {flushPassiveEffects();return null;
});
这里就是在调度useEffect()
,在layout
阶段之后会执行这个回调函数,此时会处理useEffect
的上次更新销毁函数和本次更新回调函数。
总结
看完这篇文章, 我们可以弄明白下面这几个问题:
- React的渲染流程是怎样的?
- React的beginWork都做了什么?
- React的completeWork都做了什么?
- React的commitWork都做了什么?
- useEffect和useLayoutEffect的区别是什么?
- useEffect和useLayoutEffect的销毁函数和更新回调的调用时机?
相关文章:

React源码分析(二)渲染机制
准备工作 为了方便讲解,假设我们有下面这样一段代码: function App(){const [count, setCount] useState(0)useEffect(() > {setCount(1)}, [])const handleClick () > setCount(count > count)return (<div>勇敢牛牛, <sp…...
Object.defineProperty 和 Proxy 的区别
区别:Object.defineProperty是一个用来定义对象的属性或者修改对象现有的属性的函数,,而 Proxy 是一个用来包装普通对象的对象的对象。Object.defineProperty是vue2响应式的原理, Proxy 是vue3响应式的原理1)参数不同Object.defineProperty参数obj: 要定…...

Python基础4——面向对象
目录 1. 认识对象 2. 成员方法 2.1 成员方法的定义语法 3. 构造方法 4. 其他的一些内置方法 4.1 __str__字符串方法 4.2 __lt__小于符号比较方法 4.3 __le__小于等于符号比较方法 4.4 __eq__等号比较方法 5. 封装特性 6. 继承特性 6.1 单继承 6.2 多继承 6.3 pas…...
Hive 核心知识点灵魂 16 问
本文目录 No1. 请谈一下 Hive 的特点No2. Hive 底层与数据库交互原理?No3. Hive 的 HSQL 转换为 MapReduce 的过程?No4. Hive 的两张表关联,使用 MapReduce 怎么实现?No5. 请说明 hive 中 Sort By,Order By࿰…...
聊聊探索式测试与敏捷实践
这是鼎叔的第五十二篇原创文章。行业大牛和刚毕业的小白,都可以进来聊聊。欢迎关注本专栏和微信公众号《敏捷测试转型》,大量原创思考文章陆续推出。探索式测试在敏捷测试象限中处于右上角,即面向业务且评价产品,这篇补充一下探索…...

社区宠物诊所管理系统
目录第一章概述 PAGEREF _Toc4474 \h 21.1引言 PAGEREF _Toc29664 \h 31.2开发背景 PAGEREF _Toc3873 \h 3第二章系统总体结构及开发 PAGEREF _Toc19895 \h 32.1系统的总体设计 PAGEREF _Toc6615 \h 32.2开发运行环境 PAGEREF _Toc13054 \h 3第三章数据库设计 PAGEREF _Toc2852…...

Vue项目创建首页发送axios请求
这是个全新的Vue项目,引入了ElementUI 将App.vue里的内容干掉,剩如下 然后下面的三个文件也可以删掉了 在views文件下新建Login.vue组件 到router目录下的index.js 那么现在的流程大概是这样子的 启动 写登陆页面 <template><div><el-form :ref"form"…...

Nginx
NginxNginxNginx可以从事的用途Nginx安装Nginx自带常用命令Nginx启动Nginx停止Nginx重启Nginx配置概要第一部分:全局块第二部分:events 块:第三部分:http块:Nginx Nginx是一个高性能的http和反向代理服务器࿰…...

2049. 统计最高分的节点数目
2049. 统计最高分的节点数目题目算法设计:深度优先搜索题目 传送门:https://leetcode.cn/problems/count-nodes-with-the-highest-score/ 算法设计:深度优先搜索 这题的核心是计算分数。 一个节点的分数 左子树节点数 右子树节点数 除自…...

Docker 架构简介
Docker 架构 Docker 包括三个基本概念: 镜像(Image):Docker 镜像(Image),就相当于是一个 root 文件系统。比如官方镜像 ubuntu:16.04 就包含了完整的一套 Ubuntu16.04 最小系统的 root 文件系统。容器&am…...

玄子Share-BCSP助学手册-JAVA开发
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-b2gPyAnt-1676810001349)(./assets/%E7%8E%84%E5%AD%90Share%E4%B8%89%E7%89%88.jpg)] 玄子Share-BCSP助学手册-JAVA开发 前言: 此文为玄子,复习BCSP一二期后整理的文章&#x…...

利用React实现多个场景下的鼠标跟随框提示框
前言 鼠标跟随框的作用如下图所示,可以在前端页面上,为我们后续的鼠标操作进行提示说明,提升用户的体验。本文将通过多种方式去实现,从而满足不同场景下的需求。 实现原理 实现鼠标跟随框的原理很简单,就是监听鼠标在…...

【安全知识】——如何绕过cdn获取真实ip
作者名:白昼安全主页面链接: 主页传送门创作初心: 以后赚大钱座右铭: 不要让时代的悲哀成为你的悲哀专研方向: web安全,后渗透技术每日鸡汤: 现在的样子是你想要的吗?cdn简单来说就是…...

JavaScript内存泄露和垃圾回收机制
1、是什么?内存泄露(Memory leak)是在计算机科学中,由于疏忽或错误造成程序未能释放已经不再使用的内存。并非指内存在物理上的消失,而是应用程序分配某段内存后,由于设计错误,导致在释放该段内…...

Kubernetes02:知识图谱
Kubernetes01:知识图谱 MESOS APACHE 分布式资源管理框架 2019-5 Twitter 》 Kubernetes Docker Swarm 2019-07 阿里云宣布 Docker Swarm 剔除 Kubernetes Google 10年容器化基础架构 borg Go语言 Borg 特点 轻量级:消耗资源小 开源 弹性伸缩 负载均…...

nginx-服务器banner泄漏风险
http { server_tokens off; # 隐藏Nginx版本号 .... }...

GCC 同名符号冲突解决办法
一、绪论 作为 C/C 的开发者,大多数都会清楚课本上动态库以及静态库的优缺点,在教科书上谈及到动态库的一个优点是可以节约磁盘和内存的空间,多个可执行程序通过动态库加载的方式共用一段代码段 ;而时至今日,再看看上…...

下一代视频编码技术2023
下一代视频编码技术 下面将从这两个角度来介绍华为云视频在下一代视频编码技术上的一些工作。这些技术得益于华为2012 媒体技术院全力支持。 2.1 下一代视频编码标准技术 从上图可以看出,下一代的视频编码标准大概分为三个阵营或者三个类型: 国际标准…...

最新最全中小微企业研究数据:海量创业公司信息与获取投资信息(1985-2021年)
一、企业获取投资名单&资方信息 数据来源:搜企网、企查查、天眼查 时间跨度:1985年8月-2021年9月 区域范围:全国范围 数据字段:企业名称、时间、获得投资金额以及投资方信息 部分数据: DateCompany_nameUnit…...
springboot数据源浅析
DataSourceAutoConfiguration分析 SpringBoot有一个自动配置DataSourceAutoConfiguration 为数据源配置 /META-INF/spring.factories文件找到DataSourceAutoConfiguration配置类 一、先来看下DataSourceAutoConfiguration配置类生效的时机,观察源码发现 Configura…...

黑马Mybatis
Mybatis 表现层:页面展示 业务层:逻辑处理 持久层:持久数据化保存 在这里插入图片描述 Mybatis快速入门  B:十亿(Billion) 1 B 1000 M 1B 1000M 1B1000M 参数存储精度 模型参数是固定的,但是一个参数所表示多少字节不一定,需要看这个参数以什么…...
Java如何权衡是使用无序的数组还是有序的数组
在 Java 中,选择有序数组还是无序数组取决于具体场景的性能需求与操作特点。以下是关键权衡因素及决策指南: ⚖️ 核心权衡维度 维度有序数组无序数组查询性能二分查找 O(log n) ✅线性扫描 O(n) ❌插入/删除需移位维护顺序 O(n) ❌直接操作尾部 O(1) ✅内存开销与无序数组相…...

使用分级同态加密防御梯度泄漏
抽象 联邦学习 (FL) 支持跨分布式客户端进行协作模型训练,而无需共享原始数据,这使其成为在互联和自动驾驶汽车 (CAV) 等领域保护隐私的机器学习的一种很有前途的方法。然而,最近的研究表明&…...
GitHub 趋势日报 (2025年06月08日)
📊 由 TrendForge 系统生成 | 🌐 https://trendforge.devlive.org/ 🌐 本日报中的项目描述已自动翻译为中文 📈 今日获星趋势图 今日获星趋势图 884 cognee 566 dify 414 HumanSystemOptimization 414 omni-tools 321 note-gen …...
汇编常见指令
汇编常见指令 一、数据传送指令 指令功能示例说明MOV数据传送MOV EAX, 10将立即数 10 送入 EAXMOV [EBX], EAX将 EAX 值存入 EBX 指向的内存LEA加载有效地址LEA EAX, [EBX4]将 EBX4 的地址存入 EAX(不访问内存)XCHG交换数据XCHG EAX, EBX交换 EAX 和 EB…...
【碎碎念】宝可梦 Mesh GO : 基于MESH网络的口袋妖怪 宝可梦GO游戏自组网系统
目录 游戏说明《宝可梦 Mesh GO》 —— 局域宝可梦探索Pokmon GO 类游戏核心理念应用场景Mesh 特性 宝可梦玩法融合设计游戏构想要素1. 地图探索(基于物理空间 广播范围)2. 野生宝可梦生成与广播3. 对战系统4. 道具与通信5. 延伸玩法 安全性设计 技术选…...

RabbitMQ入门4.1.0版本(基于java、SpringBoot操作)
RabbitMQ 一、RabbitMQ概述 RabbitMQ RabbitMQ最初由LShift和CohesiveFT于2007年开发,后来由Pivotal Software Inc.(现为VMware子公司)接管。RabbitMQ 是一个开源的消息代理和队列服务器,用 Erlang 语言编写。广泛应用于各种分布…...

STM32HAL库USART源代码解析及应用
STM32HAL库USART源代码解析 前言STM32CubeIDE配置串口USART和UART的选择使用模式参数设置GPIO配置DMA配置中断配置硬件流控制使能生成代码解析和使用方法串口初始化__UART_HandleTypeDef结构体浅析HAL库代码实际使用方法使用轮询方式发送使用轮询方式接收使用中断方式发送使用中…...

Scrapy-Redis分布式爬虫架构的可扩展性与容错性增强:基于微服务与容器化的解决方案
在大数据时代,海量数据的采集与处理成为企业和研究机构获取信息的关键环节。Scrapy-Redis作为一种经典的分布式爬虫架构,在处理大规模数据抓取任务时展现出强大的能力。然而,随着业务规模的不断扩大和数据抓取需求的日益复杂,传统…...