【上手调试源码系列】图解react几个核心包之间的关联
🧑💻 写在开头
点赞 + 收藏 === 学会🤣🤣🤣
🥑 你能学到什么?
希望在你阅读本篇文章之后,不会觉得浪费了时间。如果你跟着读下来,你将会学到:
- react核心基础包结构&宏观架构分层
- 两大循环,任务调度循环&fiber构造循环之间的关系
- 内核的执行流程,几个模块之间是如何串联的
- 一个task大致长什么样
✍️系列文章react实现原理系列
🍑 一、调用关系&架构分层
1.react包
React 核心的功能和API。这个包提供了创建 React 组件、状态管理、生命周期方法等核心功能。这个包对任何使用 React 的项目来说都是必须的。
- React 元素创建:
react
提供了React.createElement()
方法,允许开发者定义 UI 结构。这大多是通过 JSX 语法间接完成的,因为 JSX 会被编译为React.createElement()
调用。 - 组件和类定义: 提供了创建组件的基础机制,包括
React.Component
和React.PureComponent
基类。这些类提供了定义组件的生命周期方法、状态(state
)和属性(props
)的能力。 - Hooks: 自 React 16.8 版本起,
react
引入了 Hooks API,包括useState
,useEffect
,useContext
,useReducer
等等,使得在函数组件中也能够使用状态管理和生命周期特性。 - Context: 提供了 Context API,即
React.createContext
,Context.Provider
,Context.Consumer
等,可以不必通过 props 逐层传递,而是可以跨组件层级直接传递数据。 - 片段: React.fragment (
<>...</>
) 是一种 使组件返回多个元素的方式。 - Refs: 提供了
React.createRef
,React.forwardRef
等 API 以管理对 DOM 节点或组件实例的直接引用。 - 错误边界:
React.Component
类中的componentDidCatch
生命周期方法,允许组件截获其子组件树中 JS 错误并记录这些错误。 - 工具函数:
react
包还包含诸如React.Children
以及其他辅助函数用于处理 children props 以及其他不同种类的工具。
2.react-dom
react-dom
包是 React 生态系统中专门针对 Web 浏览器环境的包,它提供了在浏览器中与 DOM(文档对象模型)交互所需的方法和API。
- 渲染:
ReactDOM.render()
是react-dom
包最重要的方法之一,它将 React 元素渲染到指定的 DOM 容器中。React 18 版本引入了createRoot()
函数,用作同样目的,但以更先进的方式。 - 与浏览器交互: 除了渲染元素外,
react-dom
也处理事件处理程序的绑定和解绑定,以及其他需要直接与浏览器 DOM API 交互的任务。 - 生命周期钩子:
react-dom
负责调用组件的生命周期方法,如组件的挂载 (componentDidMount
), 更新 (componentDidUpdate
), 和卸载 (componentWillUnmount
) 等。 - Portals:
react-dom
提供了创建 portals 的API。Portals 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的方法。 - 服务器端渲染(SSR) :
react-dom/server
提供了用于服务器端渲染的API。这些API包括renderToString()
和renderToStaticMarkup()
,它们分别将 React 元素转换成 HTML 字符串,并可用于生成网页的初始渲染。 - 测试工具:
react-dom
提供了react-dom/test-utils
,这是一组用于协助测试组件行为的工具。 - 查找 DOM 节点:
react-dom
还提供了ReactDOM.findDOMNode()
方法,该方法能够获取到组件对应的 DOM 节点,不过出于性能和代码清晰度的原因,这个方法并不推荐使用。 - Hydration: 当服务器端渲染(SSR)被用来生成 React 组件的 HTML,并且在客户端上被加载时,
react-dom
可以 "hydration" 这些 HTML,效果上是重新连接事件处理器等,无需重新渲染 DOM。
在 React 架构中,react
库负责定义如何创建组件和管理状态,而 react-dom
库是 React 的渲染引擎,处理如何实际在浏览器中展示和更新这些组件。这种分离使得同样的 React 组件能在不同环境(如Web, Native, VR等)中运作,只需配合不同的渲染引擎(如 react-dom
, react-native
)。
3.react-reconciler
它提供了定义自定义渲染器(renderer)的能力,同时封装了 React 的调和(reconciliation)算法,也就是用来计算更新的。通过 react-reconciler
,开发者可以创建新的渲染器来将 React 组件渲染到不同的平台和渲染环境,例如 WebGL 或命令行界面(CLI)等
- 调和算法:
react-reconciler
实现了 React 的 Fiber 调和算法,这是一种用于协调和渲染 React 元素树的高效算法。它允许 React 在渲染过程中暂停、中断和复用工作,以适应主线程的其他工作。 - 平台独立性: 将调和过程从平台特定的细节中抽象出来,意味着同一套 React 代码可以用于不同平台的渲染,每个平台只需要自己的渲染器。
- 自定义渲染:
react-reconciler
允许开发者创建新的渲染后端,适用于各种输出目标。渲染器负责维护宿主环境的元素树,并响应从 React 传来的创建、更新或删除操作。 - 核心API:
react-reconciler
包提供了一个Reconciler(config)
函数。开发者可以提供一个配置对象(config
),里面包含宿主环境(如浏览器DOM、React Native等)的各种方法实现。这些方法包括创建、删除和更新宿主环境中的节点,以及插入节点等。 - 自定义组件生命周期: 此外,
react-reconciler
允许开发者实现自定义渲染器的生命周期方法,例如在特定时间点更新或清理资源。 - 实验性功能支持: 使用
react-reconciler
构建的渲染器可以访问 React 核心团队正在开发的最新功能和改进,例如提供对并发模式的支持。
4.scheduler【16之前是没有的】
它专注于优化性能,尤其是与任务调度相关的性能。React 团队引入了这个包以实现如并发模式(Concurrent Mode)这样的高级功能,它通过协调不同优先级的任务来优化渲染性能,特别是在大型更新或在主线程上执行高代价计算时。
- 任务调度:
scheduler
包允许 React 根据任务的优先级来调度任务。这意味着 React 可以决定何时执行某个任务,以及如何在重要的渲染和更新中插入更紧迫的工作。 - 优先级设置: 它提供了设置不同任务优先级的能力,这样 React 可以优先处理用户交互等高优先级任务,而将不那么紧急的任务(如数据预取)推迟到稍后执行。
- 中断和恢复工作: 在执行长时间的任务时,
scheduler
可以中断这些任务以确保主线程不会被阻塞,从而保持应用的响应性。之后,一旦主线程变得空闲,scheduler
可以恢复这些被中断的任务。 - 协调多个任务: 它可以管理并协调多个任务的执行,确保任务按照正确的顺序和优先级执行。
- 避免主线程阻塞:
scheduler
包支持浏览器的requestIdleCallback()
API,并通过这种方式允许任务在主线程空闲时执行,这减少了对主线程的阻塞,从而改善了应用程序的性能。 - 高级调度: 通过实验性的并发功能,开发者可以利用
scheduler
包在具有复杂状态逻辑和数据获取需求的大型应用程序中进行更高级的调度。
🍎 二、react中的两个循环
1.任务调度循环
任务调度循环
的逻辑主要是宏观的去调度的是每一个任务(task
),而不关心这个任务具体是干什么的, 具体任务其实就是执行回调函数。react-reconciler
收到更新需求
(新增、删除、更新)之后,并不会立即构造fiber
树,而是去scheduler
调度中心注册task
,task
进入task
队列等待合适时机进行调用执行task
中的回调
2.fiber构造循环
fiber构造循环
是以树
为数据结构, 从上至下执行深度优先遍历,构造fiber
树,并通过commitRoot(FiberRootNode)
传入react-dom
进行dom
渲染。
🍉 三、两个循环之间的关系
从上面的图中我们可以看出一个task
主要包括(fiber
树的构造、DOM
渲染、调度检测),react-reconciler
注册的task在某个时间进行调用,调用时会去实现task,其中包括
fiber tree
构造DOM
渲染- 调度检测
而
fiber
构造循环就是task
实现环节之一,任务都会重新构造一个fiber
树。
🍒 四、react的几个对象
以下例子均是以之前搭建的源码目录中的app.js
文件绘制!!!
JSX
在上两篇文章中我们已经探究过jsx
的原理,jsx
最终会被babel
转为createElement
然后创建ReactElement
ReactElement
1.基本介绍
ReactElement
是React
应用中最基础的单元。它是对 UI 部分的轻量级描述,实际上是一个普通的js
对象,可以表示DOM
节点或者其他组件的组成部分。元素(Element
)对象包含组件类型(例如div、span
或其它自定义组件)、属性以及子元素。元素是不可变的;一旦被创建,就不能更改它的子元素或属性。更新 UI 时,React 会创建一个新的元素树来表示新的UI
状态。react
会根据ReactElement去构建fiber
树
2.数据结构
{
$$typeof: Symbol.for('react.element'),
type: 'div',
key: null,
ref: null,
props: {
className: 'sidebar',
children: 'Content here' // 这可以是任何类型的子内容,包括更多的React元素。
},
_owner: null, // (已废弃)用于调试目的,指向创建该元素的组件的Fiber。
_store: {}, // (已废弃)用于存储一些额外的信息。
_self: null, // (内部使用)用于指向this上下文的引用。
_source: null, // (内部使用)用于指向元素创建时的源码位置(通常用于开发者工具)。
}
3.内存中的结构
类树状结构如下
fiber
基本介绍
Fiber
是 React 16
版本中引入的一个新的协调算法(也就是 React 的核心算法之一)。它的引入主要是为了优化 React
的渲染流程,使其可以执行更有效的更新和任务调度。Fiber 架构的引入解决了传统 React
虚拟 DOM
算法(递归比对更新策略)的一些限制,如以下几点:
- 可中断工作: 在旧的算法中,一旦开始渲染工作,就必须一气呵成,直到整个虚拟
DOM
树都比对完毕,这可能会导致主线程长时间被占用,从而影响动画、布局和输入响应的性能。Fiber 架构引入了一种能力,即可以将渲染工作分割成多个小块,实现工作的中断与恢复,使得主线程可以在需要时立即执行更紧急的任务。 - 增量渲染: 通过分割工作为一系列小任务,React 可以通过这种方式逐步完成整个渲染过程,而不是一次性完成。这样就可以将重要的更新(比如动画帧)安排在首位,从而提高应用的响应速度和性能。
- 更好的错误处理:
Fiber
允许React
能够在组件树中“捕获”子树中的错误,并将其上抛至更高的错误边界(Error Boundary
)组件。
数据结构
数据结构大致如下:
export const Fiber = {
tag: WorkTag,
key: null | string, // 和ReactElement组件的 key 一致.
elementType: any,//一般来讲和ReactElement组件的 type 一致 比如div ul
type: any, // 一般来讲和fiber.elementType一致. 一些特殊情形下, 比如在开发环境下为了兼容热更新
stateNode: any, // 真实DOM是谁
return: Fiber | null, //爹是谁
child: Fiber | null, //孩子是谁
sibling: Fiber | null, //兄弟是谁
index: number,
...
...
}
内存中的结构
源码中的定义
function FiberRootNode(
containerInfo,
tag,
hydrate,
identifierPrefix,
onRecoverableError,
) {
// 标识不同类型的根,如ReactDomRoot、ReactConcurrentRoot等
this.tag = tag;
// DOM容器节点的引用,React元素将挂载到这个DOM节点上
this.containerInfo = containerInfo;
// 对子级的悬挂状态的引用,对于服务器端渲染为true,用于客户端在hydrate时使用
this.pendingChildren = null;
// 当前活跃的根Fiber对象的引用
this.current = null;
// 用于低优先级任务的缓存处理,用于跟踪ping的lanes
this.pingCache = null;
// 指向已完成但还未提交到DOM的工作
this.finishedWork = null;
// 用于跟踪异步任务的超时动作
this.timeoutHandle = noTimeout;
// 顶层Fiber的上下文,用于提供一个全局数据传递途径
this.context = null;
// 即将被处理的上下文,也是全局数据传递途径之一
this.pendingContext = null;
// 当前被安排的回调函数的节点
this.callbackNode = null;
// 当前调度回调函数的优先等级,使用位字段表示
this.callbackPriority = NoLane;
// 一个map,记录了所有lane的事件时间
this.eventTimes = createLaneMap(NoLanes);
// 一个map,记录了所有lane的到期时间
this.expirationTimes = createLaneMap(NoTimestamp);
// 当前pending的lanes
this.pendingLanes = NoLanes;
// 当前被挂起的lanes
this.suspendedLanes = NoLanes;
// 当接收到ping时被标记的lanes
this.pingedLanes = NoLanes;
// 过期的lanes,需要立刻执行
this.expiredLanes = NoLanes;
// 可变读取的lanes,表示哪些lane正在进行数据获取
this.mutableReadLanes = NoLanes;
// 完成的lanes,表示这些lane对应的工作已经完成了
this.finishedLanes = NoLanes;
// 与其他lane相互嵌套的lanes
this.entangledLanes = NoLanes;
// 一个记录当前根与其他lane相互嵌套的map
this.entanglements = createLaneMap(NoLanes);
// 服务端渲染时的标识前缀,用于生成容器内部DOM节点的data-reactid
this.identifierPrefix = identifierPrefix;
// 一个处理可恢复错误的函数,提供给React内部使用,允许Reacoverable错误触发恢复逻辑
this.onRecoverableError = onRecoverableError;
}
🥑 五、几个问题探究
1.为什么有了reactElement数据结构还需要fiber数据结构?
- 使用目的不同,
ReactElement
用于描述UI,fiber
主要优化 React 的渲染流程,使其可以执行更有效的更新和任务调度 - 认为
ReactElement
仍然是关于组件树的宏观描述,而 Fiber 是在微观级别进行工作的引擎
2.为什么不直接扩展ReactElement?
扩展 React Element 数据结构以支持可中断的渲染和任务调度(即实现 Fiber 的功能)会存在以下几个问题:
- 不同职责分离: React Element 本质上是对用户界面的静态描述,它是相对简单的。它应当保持轻量和不可变,以保证 React 的基础功能简洁且高效。Fiber,相反,负责处理渲染过程中的状态管理、任务调度、优先级处理等动态操作。这些操作属于 React 框架的内部机制,与组件的具体描述(React Element 的职责)是不同的领域。如果将这些复杂性压在 React Element 上,会显著增加该数据结构的复杂度和难以维护。
- 向后兼容性: React 的设计强调向后兼容性。很多已有的应用依赖于当前的 React Element 表现和行为。如果大幅更改 React Element 的结构或行为,可能会造成广泛的兼容性问题,迫使现有应用进行大量重构。
- 渲染性能: React Element 作为 UI 的声明性描述,它们被大量创建和比对。如果将渲染的动态行为融入到 React Element 中,其性能可能会因增加的复杂度而受到负面影响。而 Fiber 结构可以优化这一处理流程,使性能得到改善。
- 清晰的抽象层: React 的设计哲学之一就是提供简单的 API,并隐藏复杂的实现细节。开发者使用 React Element 来定义他们的组件,而不需要关心 React 内部如何进行任务调度和状态更新。引入 Fiber 作为一个独立的内部机制,让 React 可以保持简洁的外部 API,同时拥有处理复杂场景所需的能力。
- 专门化: React Element 描述了 UI 长什么样,而 Fiber 节点不仅包含 UI 描述信息,还包含了执行更新所需的附加信息(如当前的生命周期状态、挂起的优先级更新等)。Fiber 节点包含的信息更丰富,更适合在 React 框架内部处理更新逻辑。
并且fiber
也需要知道需要绘制什么样的UI,这里只是将UI和绘制和任务调度分离,Fiber
也并没有在内部创建一个与 ReactElement
功能重叠的新数据结构。相反,它使用现有的 ReactElement
来理解应用程序的组件树以及如何渲染 UI
。
推荐阅读
工程化系列
本系列是一个从0到1的实现过程,如果您有耐心跟着实现,您可以实现一个完整的react18 + ts5 + webpack5 + 代码质量&代码风格检测&自动修复 + storybook8 + rollup + git action
实现的一个完整的组件库模板项目。如果您不打算自己配置,也可以直接clone组件库仓库切换到rollup_comp
分支即是完整的项目,当前实现已经足够个人使用,后续我们会新增webpack5优化、按需加载组件、实现一些常见的组件封装:包括但不限于拖拽排序、瀑布流、穿梭框、弹窗等
面试手写系列
react实现原理系列
其他
🍋 写在最后
如果您看到这里了,并且觉得这篇文章对您有所帮助,希望您能够点赞👍和收藏⭐支持一下作者🙇🙇🙇,感谢🍺🍺!如果文中有任何不准确之处,也欢迎您指正,共同进步。感谢您的阅读,期待您的点赞👍和收藏⭐!
感兴趣的同学可以关注下我的公众号ObjectX前端实验室
🌟 少走弯路 | ObjectX前端实验室 🛠️「精选资源|实战经验|技术洞见」
转载自:https://juejin.cn/post/7363220284503097354