React
是一个 UI 渲染框架, 提供了一种声明式的编程范式,让我们可以通过编写 JSX/TSX 来进行页面 UI 的构建,这个开发者带来了便利。
但在这个便利后面,实际上 React
框架做了很多层面的事情,整体也比较复杂。
当我们一开始读 React
源码的时候,我们可能会觉得比较吃力,其中有两个原因:
- 宏观和微观的看法:读源码的过程我们很容易陷入微观去看代码的情形,比如具体往某个函数的具体代码,但这里面有过多的细节,可能会分散我们对整个流程的理解。所以个人建议,先宏观看整体流程,再具体看细节边界。
- 过多概念需要前置了解:在读
React
源码过程中,会有非常多的概念,如Fiber
,Hook
,Lane
,并发更新/同步更新的概念。而这些如果我们对其有概念层面的理解,会更好的理解他们的作用。
所以,这里我们首先会从 React
中的一些常见的概念入手,从而来了解 React
背后的事情。
JSX
JSX
是我们日常使用 React
中接触最多的东西吧。但他究竟是什么呢。
我理解为: JSX
是一种语法糖,是 React
借助编译手段,让开发者可以用类似 HTML 的语法来写 React 代码,其最终会转换成 createElement
的函数, 而这个函数是 ReactElement
。
所以,这里注意, JSX 的语法只在编译流程使用,无法到具体的运行时中。实际上, React 并没有运行时的编译,所以的编译工作都会在编译时做好。
好的,我们了解了 JSX
, 也引出了 ReactElement
这个,那我们接着来看 ReactElement
吧。
ReactElement
JSX
是用于用语法糖来描述 UI,而 ReactElement
可以理解为某个时刻上用 JS 对象来用来描述 UI 。但 ReactElement
你可以理解为更多的是通过计算出来的属性,自身不带任何状态,只用于描述某一个时刻的 UI 情况。
具有几个特点:
- 无状态:
ReactElement
本身只是一种 UI 描述,并不存储任何与组件交互相关的状态。组件的状态管理由React
的内部机制(如State
和Context
)负责。 - 只读性:
ReactElement
是不可变对象,用于 React 的渲染和更新计算。 - 时刻性:
ReactElement
描述的是某一特定时刻的 UI 状态,任何交互或状态变化都会导致新的ReactElement
被创建,用来描述更新后的 UI。
<div>12312312</div>
{
$$typeof: Symbol(react.element),
"type": "div",
"key": null,
"ref": null,
"props": {
"children": "12312312"
},
"_owner": null,
"_store": {}
}
在举个例子
const App = () => {
return (
<div>
<h1>h1 node</h1>
<p>p node</p>
</div>
);
}
Fiber
Fiber
可能在面试过程或阅读源码的过程中,会问得比较多的一个概念吧。
但实际上 Fiber
可以有不同的理解。
-
Fiber 架构
: React 16 之后新出的一种架构,用于做 React 16 之后的reconcile
和commit
等更新流程,取代之前的Stack Reconciler
的架构,从而为之后并发更新打下基础。 -
Fiber 节点
: React 16 之后在整个 React 运行流程中,都需要使用到的一个工作单元,具体指待某个节点的信息,如- 基本信息:组件类型,组件的 key, 组件的 stateNode, 组件的 type
- 其他节点关系:如父亲节点,子节点,兄弟节点的信息,index 信息。
- 存放状态和副作用的信息:lanes, childLanes, flag, subTreeFlag, props 信息, alternate 信息,hooks 信息。
这里我们具体聊聊 Fiber 节点
可以看到精简之后的源码如下
function FiberNode(
tag: WorkTag,
pendingProps: mixed,
key: null | string,
mode: TypeOfMode,
) {
// Instance
this.tag = tag; // fiber 的类型
this.key = key; // fiber 的 key,用于后续 recociler
this.elementType = null;
this.type = null; // 函数组件的话, fiber 的 type 指向函数组件的引用
this.stateNode = null; // 存放信息,如 HostComponent 存放 dom 阶段,根节点指向 FiberRoot, class 指向 class 实例
// Fiber
this.return = null; // 父亲节点
this.child = null; // 子节点
this.sibling = null; // 下一个兄弟节点
this.index = 0; // 在父节点中的 index 索引
this.ref = null; // 指向 组件设置的 ref
this.pendingProps = pendingProps; // 输入的 props
this.memoizedProps = null; // 上一次子节点的 props
this.updateQueue = null; // 存储 update 更新对象的队列,
this.memoizedState = null; // 存放一些局部状态,如 hook
this.dependencies = null; // fiber 节点依赖的 context 等信息
this.mode = mode;
// Effects
this.flags = NoFlags; // 标记位,副作用标识
this.subtreeFlags = NoFlags; // 子数的副作用标识
this.deletions = null; // 需要删除的子 fiber
this.lanes = NoLanes; // lane 优先级标识
this.childLanes = NoLanes; // childLanes 子孙节点的优先级标识
this.alternate = null; // wip's alternate === current, current's alternate === wip
}
这里我们还是用上方的例子。
const App = () => {
return (
<div>
<h1>h1 node</h1>
<p>p node</p>
</div>
);
}
好了,上方我们也简单讲下 Fiber Tree
的结构。
在这里,我们停一下,捋捋 JSX
、 ReactElement
、 Fiber
的关系。
JSX
-> ReactElement
-> Fiber
的转换关系:
- JSX 是写代码时的语法糖,编译后变成
React.createElement()
调用,执行后生成ReactElement
对象 ReactElement
是纯对象,包含组件的type
、props
、children
等信息,形成不可变的树形结构Fiber
是基于ReactElement
创建的可变节点,通过child/sibling/return
相连,额外包含了状态、更新队列等信息,支持中断和恢复渲染
过程是:从写代码时的 JSX,到运行时的 ReactElement 描述,最后转换为支持异步渲染的 Fiber 节点。
好的,接着我们来了解一个陌生的名词, FiberRootNode
FiberRootNode
FiberRootNode 是 React 应用的总根节点,负责管理整个 React 应用树。
它的主要作用包括:
- 持有并切换渲染树(current 和 workInProgress Fiber 树)
- 调度更新(包括更新队列和优先级)
- 管理全局状态(如事件系统、并发特性)
这里说说第一点吧,如果你对 react
有所理解,你会发现 React
在更新过程是采用了双缓存策略,从而做渲染切换。
基本结构
更新流程:会涉及到 fiber 树切换,下方是一个从 h1 → h2 切换的流程。
Hook
接着,我们来讲讲 Hook 这个概念。对于 Hook ,我是这么理解的: “React 官方所提供的一套机制,可以使用 React 提供的函数去使用 React 的内置逻辑,如管理组件的状态和副作用”
主要有下方几点功能:
- 管理组件状态:hook 的出现,使得函数组件开始有了状态的概念,编写起来更为友好。
- 管理组件副作用:由于 React 倡导声明式编程,这和副作用其实有一定的冲突,所以,我们需要有一个机制,能够去执行副作用, React 则提供了 useEffect, useLayoutEffect 的能力。
- 逻辑更好进行复用: React Hook 的出现,使得以往 React 组件粒度的复用逻辑,可以变成状态,副作用管理的逻辑,粒度更为细,更容易进行复用。
- 自定义 Hook:允许开发者根据自己的需求封装复用工具,减少代码重复度。
具体例子:
useState
:管理组件的局部状态。useEffect
:替代生命周期方法(如componentDidMount
和componentDidUpdate
)。useContext
:获取上下文。useReducer
:替代 Redux 等场景下的状态管理。useMemo
和useCallback
:性能优化工具,防止不必要的渲染或函数创建。useRef
:访问 DOM 元素或保存某些不参与渲染的变量。
Lane
Lane
是 React 用于标识更新优先级的地方。当我们每次进行更新的时候,实际上我们都会计算出一个 Lane
, 从而判断当前更新的优先级。从而确定后续我们要走的更新策略。
本质上每次更新会存在一个转化流程: → Lane
→ schedulerPriority
从而决定 scheduler
是异步调度还是直接运行,以及后续要做并发更新还是同步更新。
时间分片
React
中有一个概念,就是时间分片,其本质就是将一个任务拆分成多个任务,从而先保证更高优先级交互或者用户的操作行为,提高交互体验。
但我们这里需要思考几个点?
-
优化交互体验,为什么会存在交互体验问题
浏览器的一个事件循环中,会包括 JS 执行,渲染等工作,只有当浏览器能够保持在 1s 内渲染 60 帧数的时候,我们人眼会觉得不会卡顿。但 JS 任务时一个单线程任务,这意味在排除协程的情况下, JS 的一段代码开始执行,就会占用渲染进程的主线程,并且等到主线程结束。所以但 JS 是一个 LongTask 的时候,这个时候应该是达不到 60 帧的,这个时候会有卡顿效果。
-
时间分片为什么能解决这个问题?
上面我们知道,卡顿的原因可能是 JS 的长任务的问题。所以我们只需要把 JS 的耗时给缩短就好了,这里的时候,实际上就是讲一个长任务,拆分成多个子任务即可。
这实际上也就是 React 实现并发更新的思路,所以时间分片实际上是 React 的基础。
并发更新和同步更新
React 16 之后,其实内部已经有了并发更新的 API,但是一直没对外暴露,而是在 React 18 的正式版本重才真正对外。
所以,我们有几个问题
-
我们为什么需要并发更新呢?
因为
React
在进行复杂组件的渲染过程中,可能会有性能问题,交互不友好。 -
那么为什么之前的
React
会有这种交互体验的影响?本质上还是一个
LongTask
的问题,在并发更新模式没出来之前,React 一旦开始进行了reconcile
阶段,就无法中断,而当这个过程阶段特别长的时候,相当于是一个LongTask
,所以会导致卡顿。 -
为什么并发更新如何解决这个问题?
并发更新做了些优化,本质上还是通过
React
的Fiber
架构实现了更中断的能力和时间分片的机制
做结合,使得React
在并发更新下的reconcile
流程可以中断(但commit
阶段无法中断), 从而减少LongTask
阻塞问题,让主线程能够执行布局计算,渲染等工作。
总结来说:
-
思路:并发更新实际上解决的还是 LongTask 的问题,将一个任务拆分到多个事件循环中,保证其他优先级高的事件先执行。
-
具体措施:
Fiber
架构下reconcile
的可中断 + 时间分片机制