React 基本概念

131 阅读9分钟

React 是一个 UI 渲染框架, 提供了一种声明式的编程范式,让我们可以通过编写 JSX/TSX 来进行页面 UI 的构建,这个开发者带来了便利。

但在这个便利后面,实际上 React 框架做了很多层面的事情,整体也比较复杂。

当我们一开始读 React 源码的时候,我们可能会觉得比较吃力,其中有两个原因:

  • 宏观和微观的看法:读源码的过程我们很容易陷入微观去看代码的情形,比如具体往某个函数的具体代码,但这里面有过多的细节,可能会分散我们对整个流程的理解。所以个人建议,先宏观看整体流程,再具体看细节边界。
  • 过多概念需要前置了解:在读 React 源码过程中,会有非常多的概念,如 FiberHookLane,并发更新/同步更新的概念。而这些如果我们对其有概念层面的理解,会更好的理解他们的作用。

所以,这里我们首先会从 React 中的一些常见的概念入手,从而来了解 React 背后的事情。

JSX

JSX 是我们日常使用 React 中接触最多的东西吧。但他究竟是什么呢。

我理解为: JSX 是一种语法糖,是 React 借助编译手段,让开发者可以用类似 HTML 的语法来写 React 代码,其最终会转换成 createElement 的函数, 而这个函数是 ReactElement

image.png

所以,这里注意, JSX 的语法只在编译流程使用,无法到具体的运行时中。实际上, React 并没有运行时的编译,所以的编译工作都会在编译时做好。

好的,我们了解了 JSX, 也引出了 ReactElement 这个,那我们接着来看 ReactElement 吧。

ReactElement

JSX 是用于用语法糖来描述 UI,而 ReactElement 可以理解为某个时刻上用 JS 对象来用来描述 UI 。但 ReactElement 你可以理解为更多的是通过计算出来的属性,自身不带任何状态,只用于描述某一个时刻的 UI 情况。

具有几个特点:

  • 无状态ReactElement 本身只是一种 UI 描述,并不存储任何与组件交互相关的状态。组件的状态管理由 React 的内部机制(如 StateContext)负责。
  • 只读性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>
  );
}

image.png

Fiber

Fiber 可能在面试过程或阅读源码的过程中,会问得比较多的一个概念吧。

但实际上 Fiber 可以有不同的理解。

  • Fiber 架构: React 16 之后新出的一种架构,用于做 React 16 之后的 reconcilecommit 等更新流程,取代之前的 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>
  );
}

image.png

好了,上方我们也简单讲下 Fiber Tree 的结构。

在这里,我们停一下,捋捋 JSXReactElementFiber 的关系。

image.png

JSX -> ReactElement -> Fiber 的转换关系:

  1. JSX 是写代码时的语法糖,编译后变成 React.createElement() 调用,执行后生成 ReactElement 对象
  2. ReactElement 是纯对象,包含组件的 typepropschildren 等信息,形成不可变的树形结构
  3. Fiber 是基于 ReactElement 创建的可变节点,通过 child/sibling/return 相连,额外包含了状态、更新队列等信息,支持中断和恢复渲染

过程是:从写代码时的 JSX,到运行时的 ReactElement 描述,最后转换为支持异步渲染的 Fiber 节点。

好的,接着我们来了解一个陌生的名词, FiberRootNode

FiberRootNode

FiberRootNode 是 React 应用的总根节点,负责管理整个 React 应用树。

它的主要作用包括:

  • 持有并切换渲染树(current 和 workInProgress Fiber 树)
  • 调度更新(包括更新队列和优先级)
  • 管理全局状态(如事件系统、并发特性)

这里说说第一点吧,如果你对 react 有所理解,你会发现 React 在更新过程是采用了双缓存策略,从而做渲染切换。

基本结构

image.png

更新流程:会涉及到 fiber 树切换,下方是一个从 h1 → h2 切换的流程。

image.png

Hook

接着,我们来讲讲 Hook 这个概念。对于 Hook ,我是这么理解的: “React 官方所提供的一套机制,可以使用 React 提供的函数去使用 React 的内置逻辑,如管理组件的状态和副作用”

主要有下方几点功能:

  • 管理组件状态:hook 的出现,使得函数组件开始有了状态的概念,编写起来更为友好。
  • 管理组件副作用:由于 React 倡导声明式编程,这和副作用其实有一定的冲突,所以,我们需要有一个机制,能够去执行副作用, React 则提供了 useEffect, useLayoutEffect 的能力。
  • 逻辑更好进行复用: React Hook 的出现,使得以往 React 组件粒度的复用逻辑,可以变成状态,副作用管理的逻辑,粒度更为细,更容易进行复用。
  • 自定义 Hook:允许开发者根据自己的需求封装复用工具,减少代码重复度。

具体例子:

  • useState:管理组件的局部状态。
  • useEffect:替代生命周期方法(如 componentDidMountcomponentDidUpdate)。
  • useContext:获取上下文。
  • useReducer:替代 Redux 等场景下的状态管理。
  • useMemouseCallback:性能优化工具,防止不必要的渲染或函数创建。
  • useRef:访问 DOM 元素或保存某些不参与渲染的变量。

Lane

Lane 是 React 用于标识更新优先级的地方。当我们每次进行更新的时候,实际上我们都会计算出一个 Lane , 从而判断当前更新的优先级。从而确定后续我们要走的更新策略。

本质上每次更新会存在一个转化流程: → LaneschedulerPriority

从而决定 scheduler 是异步调度还是直接运行,以及后续要做并发更新还是同步更新。

时间分片

React 中有一个概念,就是时间分片,其本质就是将一个任务拆分成多个任务,从而先保证更高优先级交互或者用户的操作行为,提高交互体验。

image.png

但我们这里需要思考几个点

  1. 优化交互体验,为什么会存在交互体验问题

    浏览器的一个事件循环中,会包括 JS 执行,渲染等工作,只有当浏览器能够保持在 1s 内渲染 60 帧数的时候,我们人眼会觉得不会卡顿。但 JS 任务时一个单线程任务,这意味在排除协程的情况下, JS 的一段代码开始执行,就会占用渲染进程的主线程,并且等到主线程结束。所以但 JS 是一个 LongTask 的时候,这个时候应该是达不到 60 帧的,这个时候会有卡顿效果。

  2. 时间分片为什么能解决这个问题?

    上面我们知道,卡顿的原因可能是 JS 的长任务的问题。所以我们只需要把 JS 的耗时给缩短就好了,这里的时候,实际上就是讲一个长任务,拆分成多个子任务即可。

这实际上也就是 React 实现并发更新的思路,所以时间分片实际上是 React 的基础。

并发更新和同步更新

React 16 之后,其实内部已经有了并发更新的 API,但是一直没对外暴露,而是在 React 18 的正式版本重才真正对外。

所以,我们有几个问题

  1. 我们为什么需要并发更新呢?

    因为 React 在进行复杂组件的渲染过程中,可能会有性能问题,交互不友好。

  2. 那么为什么之前的 React 会有这种交互体验的影响?

    本质上还是一个 LongTask 的问题,在并发更新模式没出来之前,React 一旦开始进行了 reconcile 阶段,就无法中断,而当这个过程阶段特别长的时候,相当于是一个 LongTask ,所以会导致卡顿。

  3. 为什么并发更新如何解决这个问题?

    并发更新做了些优化,本质上还是通过 ReactFiber 架构实现了更中断的能力时间分片的机制 做结合,使得 React 在并发更新下的 reconcile 流程可以中断(但 commit 阶段无法中断), 从而减少 LongTask 阻塞问题,让主线程能够执行布局计算,渲染等工作。

总结来说:

  • 思路:并发更新实际上解决的还是 LongTask 的问题,将一个任务拆分到多个事件循环中,保证其他优先级高的事件先执行。

  • 具体措施Fiber 架构下 reconcile 的可中断 + 时间分片机制

参考资料

7km.top/main/macro-…

github.com/BetaSu/big-…

react.iamkasong.com/

react.dev/

github.com/facebook/re…