1
头图

深入理解 React 源码,带你从零实现 React v18 的核心功能,构建自己的 React 库。

电子书地址:https://2xiao.github.io/leetcode-js/react

源代码地址:https://github.com/2xiao/my-react

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。

1. Reconciler 简介

react-2.png

在前端框架出现之前,通常会使用 jQuery 这样的库来开发页面。jQuery 是一个过程驱动的库,开发者需要直接调用浏览器的宿主环境 API,例如 DOM 操作等。这意味着开发者需要手动管理页面状态和交互,通过执行一系列的操作来更新页面。

然而,随着前端框架的出现,工作方式发生了根本性的变化,从过程驱动转变为状态驱动。在状态驱动的模式下,开发者不再直接操作宿主环境 API,而是通过前端框架提供的运行时核心模块来管理页面状态和更新。这些核心模块,例如 React 中的 Reconciler 和 Vue 中的 Renderer,负责将开发者编写的代码翻译成对应的宿主环境 API 调用,以更新页面。

Reconciler 的中文名叫协调器,它负责处理 React 元素的更新并在内部构建虚拟 DOM,这个过程是 React 框架实现高效的 UI 渲染和更新的核心逻辑所在。以下是 Reconciler 主要做的事情:

  1. 接收并解析 React 元素: Reconciler 接收 JSX 或者 createElement 函数返回的 React 元素,并将其解析成虚拟 DOM 树的结构。
  2. 协调更新: 比较新旧虚拟 DOM 树的差异,确定哪些部分需要更新,并生成更新计划。
  3. 构建虚拟 DOM 树: 在组件更新时,根据生成的更新计划,Reconciler 会更新虚拟 DOM 树的结构以反映最新的组件状态。
  4. 生成 DOM 更新指令: 将更新后的虚拟 DOM 树转换为真实的 DOM 更新指令,描述了如何将变更应用到实际的 DOM 树上。

我们先在 packages 目录下新建一个 react-reconciler 文件夹,执行 npm init,并更改配置文件如下,执行 pnpm i 安装依赖。

// packages/react-reconciler/package.json
{
    "name": "react-reconciler",
    "version": "1.0.0",
    "description": "react reconciler",
    "module": "index.ts",
    "dependencies": {
        "shared": "workspace: *"
    },
    "scripts": {
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "",
    "license": "ISC"
}

2. 实现 FiberNode

FiberNode(纤维节点)是 Reconciler 的核心数据结构之一,用于构建协调树。Reconciler 使用 FiberNode 来表示 React 元素树中的节点,并通过比较 Fiber 树的差异,找出需要进行更新的部分,生成更新指令,来实现 UI 的渲染和更新。

每个 FiberNode 都表示着 React 元素树中的一个节点,它包含了以下几个重要的字段:

  • type:节点的类型,可以是原生 DOM 元素、函数组件或类组件等;
  • props:节点的属性,包括 DOM 元素的属性、函数组件的 props 等;
  • stateNode:节点对应的实际 DOM 节点或组件实例;
  • child:指向节点的第一个子节点;
  • sibling:指向节点的下一个兄弟节点;
  • return:指向节点的父节点;
  • alternate:指向节点的备份节点,用于在协调过程中进行比较;
  • effectTag:表示节点的副作用类型,如更新、插入、删除等;
  • pendingProps:表示节点的新属性,用于在协调过程中进行更新。

接下来我们来实现一下 FiberNode 这个数据结构。

packages/react-reconciler/src/ 目录下新建 fiber.ts 文件,用来实现 FiberNode 类;新建 workTags.ts 文件,用于标识不同类型的工作单元;新建 fiberFlags.ts 文件,用于标识不同类型的副作用;代码如下:

// packages/react-reconciler/src/fiber.ts
import { Props, Key, Ref } from 'shared/ReactTypes';
import { WorkTag } from './workTags';
import { NoFlags, Flags } from './fiberFlags';

export class FiberNode {
    tag: WorkTag;
    key: Key;
    stateNode: any;
    type: any;
    return: FiberNode | null;
    sibling: FiberNode | null;
    child: FiberNode | null;
    index: number;
    ref: Ref;
    pendingProps: Props;
    memoizedProps: Props | null;
    memoizedState: any;
    alternate: FiberNode | null;
    flags: Flags;
    subtreeFlags: Flags;
    updateQueue: unknown;

    constructor(tag: WorkTag, pendingProps: Props, key: Key) {
        // 类型
        this.tag = tag;
        this.key = key;
        this.ref = null;
        this.stateNode = null; // 节点对应的实际 DOM 节点或组件实例
        this.type = null; // 节点的类型,可以是原生 DOM 元素、函数组件或类组件等

        // 构成树状结构
        this.return = null; // 指向节点的父节点
        this.sibling = null; // 指向节点的下一个兄弟节点
        this.child = null; // 指向节点的第一个子节点
        this.index = 0; // 索引

        // 作为工作单元
        this.pendingProps = pendingProps; // 表示节点的新属性,用于在协调过程中进行更新
        this.memoizedProps = null; // 已经更新完的属性
        this.memoizedState = null; // 更新完成后新的 State

        this.alternate = null; // 指向节点的备份节点,用于在协调过程中进行比较
        this.flags = NoFlags; // 表示节点的副作用类型,如更新、插入、删除等
        this.subtreeFlags = NoFlags; // 表示子节点的副作用类型,如更新、插入、删除等
        this.updateQueue = null; // 更新计划队列
    }
}
// packages/react-reconciler/src/workTags.ts
export type WorkTag =
    | typeof FunctionComponent
    | typeof HostRoot
    | typeof HostComponent
    | typeof HostText;

export const FunctionComponent = 0;
export const HostRoot = 3;
export const HostComponent = 5;
export const HostText = 6;
// packages/react-reconciler/src/fiberFlags.ts
export type Flags = number;

export const NoFlags = 0b0000000;
export const PerformedWork = 0b0000001;
export const Placement = 0b0000010;
export const Update = 0b0000100;
export const ChildDeletion = 0b0001000;

3. 实现 Reconciler 工作流程

react-3.png

Reconciler 的工作流程总的来说就是对 Fiber 树进行一次 深度优先遍历(DFS) ,首先访问根节点,然后依次访问左子树和右子树,通过比较新节点(新生成的 React Element)和旧节点(现有的 FiberNode),生成更新计划,并打上不同的标记。

  1. 遍历 Fiber 树:
    React 使用深度优先搜索(DFS)算法来遍历 Fiber 树,首先会从 Fiber 树的根节点开始进行遍历,遍历整个组件树的结构。
  2. 比较新旧节点:
    对于每个 Fiber 节点,Reconciler 会比较新节点(即新的 React Element)和旧节点(即现有的 FiberNode)之间的差异,比较的内容包括节点类型、属性、子节点等方面的差异。
  3. 生成更新计划:
    根据比较的结果,Reconciler 会生成一个更新计划,用于确定需要进行的操作,更新计划通常包括哪些节点需要更新、哪些节点需要插入到 DOM 中、哪些节点需要删除等信息。
  4. 打标记(Tagging):
    为了记录不同节点的操作,React 会为每个节点打上不同的标记。例如,如果节点需要更新,可能会打上更新标记(Update Tag);如果节点是新创建的,可能会打上插入标记(Placement Tag);如果节点被移除,可能会打上删除标记(Deletion Tag)等。
  5. 更新 Fiber 节点:
    根据生成的更新计划和标记,Reconciler 会更新对应的 Fiber 节点以反映组件的最新状态。更新操作可能包括更新节点的状态、更新节点的属性、调用生命周期方法等。
  6. 递归处理子节点:
    对于每个节点的子节点,React 会递归地重复进行上述的比较和更新操作,以确保整个组件树都得到了正确的处理。

当所有 React Element 都比较完成之后,会生成一棵新的 Fiber 树,此时,一共存在两棵 Fiber 树:

  • current: 与视图中真实 UI 对应的 Fiber 树,当 React 开始新的一轮渲染时,会使用 current 作为参考来比较新的树与旧树的差异,决定如何更新 UI;
  • workInProgress: 触发更新后,正在 Reconciler 中计算的 Fiber 树,一旦 workInProgress 上的更新完成,它将会被提交为新的current,成为下一次渲染的参考树,并清空旧的 current 树。

下面我们来实现一下 Reconciler 的完整工作流程。

packages/react-reconciler/src/ 目录下新建 workLoop.ts 文件:

// packages/react-reconciler/src/workLoop.ts
import { FiberNode } from './fiber';

let workInProgress: FiberNode | null = null;

function renderRoot(root: FiberNode) {
    prepareFreshStack(root);
    do {
        try {
            workLoop();
            break;
        } catch (e) {
            console.warn('workLoop发生错误:', e);
            workInProgress = null;
        }
    } while (true);
}

// 初始化 workInProgress 变量
function prepareFreshStack(root: FiberNode) {
    workInProgress = root;
}

// 深度优先遍历,向下递归子节点
function workLoop() {
    while (workInProgress !== null) {
        performUnitOfWork(workInProgress);
    }
}

function performUnitOfWork(fiber: FiberNode) {
    // 比较并返回子 FiberNode
    const next = beginWork(fiber);
    fiber.memoizedProps = fiber.pendingProps;

    if (next == null) {
        // 没有子节点,则遍历兄弟节点或父节点
        completeUnitOfWork(fiber);
    } else {
        // 有子节点,继续向下深度遍历
        workInProgress = next;
    }
}

// 深度优先遍历,向下递归子节点
function completeUnitOfWork(fiber: FiberNode) {
    let node: FiberNode | null = fiber;
    do {
        // 生成更新计划
        completeWork(node);
        // 有兄弟节点,则遍历兄弟节点
        const sibling = node.sibling;
        if (sibling !== null) {
            workInProgress = sibling;
            return;
        }
        // 否则向上返回,遍历父节点
        node = node.return;
        // workInProgress 最终指向根节点
        workInProgress = node;
    } while (node !== null);
}

至此,我们就完成了 Reconciler 的大致工作流程,下一节将深入实现 Reconciler 的更新机制。

相关代码可在 git tag v1.3 查看,地址:https://github.com/2xiao/my-react/tree/v1.3


《自己动手写 React 源码》遵循 React 源码的核心思想,通俗易懂的解析 React 源码,带你从零实现 React v18 的核心功能。

学完本书,你将有这些收获:

  • 面试加分:框架底层原理是面试必问环节,熟悉 React 源码会为你的面试加分,也会为你拿下 offer 增加不少筹码;
  • 提升开发效率:熟悉 React 源码之后,会对 React 的运行流程有新的认识,让你在日常的开发中,对性能优化、使用技巧和 bug 解决更加得心应手;
  • 巩固基础知识:学习本书也顺便巩固了数据结构和算法,如 reconciler 中使用了 fiber、update、链表等数据结构,diff 算法要考虑怎样降低对比复杂度;

本书的特色:

  • 教程详细,代码开源,带你构建自己的 React 库;
  • 功能全面,可跑通官方测试用例;
  • 按 Git Tag 划分迭代步骤,记录每个功能的实现过程;

电子书地址:https://2xiao.github.io/leetcode-js/react

源代码地址:https://github.com/2xiao/my-react

送我一个免费的 ⭐️ Star,这是对我最大的鼓励和支持。


TNTWEB
3.8k 声望8.5k 粉丝

腾讯新闻前端团队