深入React设计原理——从零开始学习React源码

565 阅读5分钟

React 是当前最受欢迎的前端框架之一,其以简洁的组件化思想和强大的性能优化机制,广泛应用于现代 Web 开发中。想要深入理解React的设计原理,学习源码是最好的方式。

createElement、jsx与jsxDev

在 React 中,createElementjsxjsxDEV 都是创建 React 元素的函数,它们之间的主要区别在于它们的用途和使用环境。每个函数在不同的场景下有特定的作用,通常由编译器(如 Babel)生成,以适应开发和生产环境。

以下面的 jsx 代码为例:

const element = <h1>Hello, world!</h1>;

经过编译后会变成:

const element = React.createElement('h1', null, 'Hello, world!');

React.createElement 参数

  • type:元素类型(如 divh1,或是自定义组件)
  • props:元素的属性对象,包含传递给元素的所有属性(如 idclassName 等)
  • children:子元素,作为后续参数传递

React.createElement 会返回一个 React 元素对象,如下所示:

{
  type: 'h1',
  props: {
    children: 'Hello, world!'
  },
  key: null,
  ref: null,
  _owner: null,
  _store: {}
}

jsx 函数是在 React 17 之后引入的,它用于简化 JSX 的创建流程。相比于 createElement,它更加高效,因为它可以直接创建元素对象,而不需要在内部做复杂的校验。这也是 React 17 之后推荐的用于生产环境的方式。

示例代码:

function App() {
  return <h1>Hello World</h1>;
}

编译后的代码:

import {jsx as _jsx} from 'react/jsx-runtime';

function App() {
  return _jsx('h1', { children: 'Hello world' });
}

jsxDEV 是专门为开发环境设计的函数,提供了额外的调试信息(如文件名、行号和列号)。当发生错误时,这些信息可以帮助开发者快速定位问题。

React Fiber架构

React Fiber 是 React 16 引入的一种新的协调机制,将同步递归无法中断的更新,重构为异步的可中断更新,本质上是为了解决React的性能问题。

Fiber是React中的核心数据结构,Fiber包含三层含义:

  1. 作为架构来说,之前React15Reconciler采用递归的方式执行,数据保存在递归调用栈中,所以被称为stack ReconcilerReact16Reconciler基于Fiber节点实现,被称为Fiber Reconciler
  2. 作为静态的数据结构来说,每个Fiber节点对应一个React element,保存了该组件的类型(函数组件/类组件/原生组件等)、对应的DOM节点等信息。
  3. 作为动态的工作单元来说,每个Fiber节点保存了本次更新中该组件改变的状态、要执行的工作(需要被删除/被插入页面中/被更新等)。

Fiber结点的属性如下:

export class FiberNode {
    type: any;
    tag: WorkTag;
    pendingProps: Props;
    key: Key;
    stateNode: any;
    ref: Ref;

    return: FiberNode | null;
    sibling: FiberNode | null;
    child: FiberNode | null;
    index: number;

    memoizedProps: Props | null;
    memoizedState: any;
    alternate: FiberNode | null;
    flags: Flags;
    subtreeFlags: Flags;
    updateQueue: unknown;
    deletions: FiberNode[] | null;

    constructor(tag: WorkTag, pendingProps: Props, key: Key) {
        // 实例
        this.tag = tag;
        this.key = key;
        this.stateNode = null;
        this.type = null;

        // 构成树状结构
        this.return = null;
        this.sibling = null;
        this.child = null;
        this.index = 0;

        this.ref = null;

        // 作为工作单元
        this.pendingProps = pendingProps;
        this.memoizedProps = null;
        this.memoizedState = null;
        this.updateQueue = null;

        this.alternate = null;

        // 副作用
        this.flags = NoFlags;
        this.subtreeFlags = NoFlags;
        this.deletions = null;
    }
}

FiberNode 类可以根据功能分为几个关键类别:

1. 结构相关

  • 树结构:

    • returnsiblingchild 用于构建 Fiber 树,表示父节点、兄弟节点和子节点。
    • index 标识节点在兄弟节点中的位置。
  • 标识和引用:

    • key 用于唯一标识节点,ref 用于获取组件或 DOM 元素的引用。

2. 类型和实例

  • 类型和实例:

    • type 表示节点的类型(如组件类、函数组件、DOM 元素)。
    • stateNode 是节点对应的实例(如组件实例或 DOM 元素)。

3. 属性和状态

  • 属性和状态管理:

    • pendingProps 是当前更新的属性。
    • memoizedProps 和 memoizedState 存储上次渲染的属性和状态,用于比较和决定是否需要更新。
    • updateQueue 管理与节点相关的更新。

4. 双缓存和替代

  • 双缓存机制:

    • alternate 指向当前节点的替代节点,支持 React 的双缓存更新策略。

5. 副作用和更新

  • 副作用管理:

    • flags 和 subtreeFlags 用于标记节点及其子树的副作用(如更新、删除)。
    • deletions 存储需要删除的子节点列表。

React更新流程

React架构可以分为下面三个部分:

  • Scheduler(调度器):负责管理任务的优先级和调度,决定何时执行更新任务
  • Reconciler(协调器):在 Scheduler 的指导下执行更新计算,确定哪些组件需要更新并生成 Fiber 树
  • Renderer(渲染器):将 Reconciler 生成的 Fiber 树转换为实际的 DOM 更新,执行具体的渲染操作

当组件的状态或属性发生变化时,首先由 Scheduler 负责管理任务的优先级,通过时间切片和中断机制确保高优先级任务的快速响应。接着,Reconciler 生成新的 Fiber 树,利用 Diff 算法高效地比较新旧节点,标记需要更新的节点,并收集副作用。最后,Renderer 在提交阶段执行真实的 DOM 更新,将变化应用到用户界面,并处理在 Reconciler 中收集的副作用,确保 UI 的一致性和准确性。这一流程的协作使得 React 能够高效地应对复杂应用的渲染需求,优化性能和用户体验。React更新流程如下图所示:

React更新流程.png

通过这种分工,React 可以在复杂应用中高效地管理和执行更新,确保用户界面的快速响应和一致性。调度器确保了任务的合理安排,协调器优化了更新计算,而渲染器则高效地执行了实际的 DOM 操作。