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是一个项目的根节点。我们在开始真正的渲染前会先基于rootDOM创建fiberRoot,且fiberRoot.current = rootFiber,这里的rootFiber就是currentfiber树的根节点。
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树,一个是currentfiber树,另一个是workInProgressfiber树。currentfiber树的根节点在上面已经创建,下面会通过拷贝fiberRoot.current的形式创建workInProgressfiber树的根节点。
到这里,前面的准备工作就做完了, 接下来进入正菜,开始进行循环遍历,生成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生成的不完全的workInProgressfiber树向上查找,直到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中生成的workInProgressfiber树赋值给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树插入到rootDOM节点中。
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阶段, 此时的currentfiber树还是指向更新前的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…...
docker详细操作--未完待续
docker介绍 docker官网: Docker:加速容器应用程序开发 harbor官网:Harbor - Harbor 中文 使用docker加速器: Docker镜像极速下载服务 - 毫秒镜像 是什么 Docker 是一种开源的容器化平台,用于将应用程序及其依赖项(如库、运行时环…...
React Native在HarmonyOS 5.0阅读类应用开发中的实践
一、技术选型背景 随着HarmonyOS 5.0对Web兼容层的增强,React Native作为跨平台框架可通过重新编译ArkTS组件实现85%以上的代码复用率。阅读类应用具有UI复杂度低、数据流清晰的特点。 二、核心实现方案 1. 环境配置 (1)使用React Native…...
Caliper 配置文件解析:config.yaml
Caliper 是一个区块链性能基准测试工具,用于评估不同区块链平台的性能。下面我将详细解释你提供的 fisco-bcos.json 文件结构,并说明它与 config.yaml 文件的关系。 fisco-bcos.json 文件解析 这个文件是针对 FISCO-BCOS 区块链网络的 Caliper 配置文件,主要包含以下几个部…...
Web 架构之 CDN 加速原理与落地实践
文章目录 一、思维导图二、正文内容(一)CDN 基础概念1. 定义2. 组成部分 (二)CDN 加速原理1. 请求路由2. 内容缓存3. 内容更新 (三)CDN 落地实践1. 选择 CDN 服务商2. 配置 CDN3. 集成到 Web 架构 …...
Python Ovito统计金刚石结构数量
大家好,我是小马老师。 本文介绍python ovito方法统计金刚石结构的方法。 Ovito Identify diamond structure命令可以识别和统计金刚石结构,但是无法直接输出结构的变化情况。 本文使用python调用ovito包的方法,可以持续统计各步的金刚石结构,具体代码如下: from ovito…...
2025年渗透测试面试题总结-腾讯[实习]科恩实验室-安全工程师(题目+回答)
安全领域各种资源,学习文档,以及工具分享、前沿信息分享、POC、EXP分享。不定期分享各种好玩的项目及好用的工具,欢迎关注。 目录 腾讯[实习]科恩实验室-安全工程师 一、网络与协议 1. TCP三次握手 2. SYN扫描原理 3. HTTPS证书机制 二…...
Vue ③-生命周期 || 脚手架
生命周期 思考:什么时候可以发送初始化渲染请求?(越早越好) 什么时候可以开始操作dom?(至少dom得渲染出来) Vue生命周期: 一个Vue实例从 创建 到 销毁 的整个过程。 生命周期四个…...
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析
Java求职者面试指南:Spring、Spring Boot、Spring MVC与MyBatis技术解析 一、第一轮基础概念问题 1. Spring框架的核心容器是什么?它的作用是什么? Spring框架的核心容器是IoC(控制反转)容器。它的主要作用是管理对…...
FFmpeg avformat_open_input函数分析
函数内部的总体流程如下: avformat_open_input 精简后的代码如下: int avformat_open_input(AVFormatContext **ps, const char *filename,ff_const59 AVInputFormat *fmt, AVDictionary **options) {AVFormatContext *s *ps;int i, ret 0;AVDictio…...
StarRocks 全面向量化执行引擎深度解析
StarRocks 全面向量化执行引擎深度解析 StarRocks 的向量化执行引擎是其高性能的核心设计,相比传统行式处理引擎(如MySQL),性能可提升 5-10倍。以下是分层拆解: 1. 向量化 vs 传统行式处理 维度行式处理向量化处理数…...
