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

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

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

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

第 4 节 中,我们提到 React 更新流程有四个阶段:

  • 触发更新(Update Trigger)
  • 调度阶段(Schedule Phase)
  • 协调阶段(Reconciliation Phase)
  • 提交阶段(Commit Phase)

之前我们已经实现了协调阶段(Reconciliation Phase)的 beginWorkcompleteWork 函数,接下来我们会实现提交阶段(Commit Phase)。

提交阶段的主要任务是将更新同步到实际的 DOM 中,执行 DOM 操作,例如创建、更新或删除 DOM 元素,反映组件树的最新状态,可以分为三个主要的子阶段:

  • Before Mutation (布局阶段):

    主要用于执行 DOM 操作之前的准备工作,包括类似 getSnapshotBeforeUpdate 生命周期函数的处理。在这个阶段会保存当前的布局信息,以便在后续的 DOM 操作中能够进行比较和优化。

  • Mutation (DOM 操作阶段):

    执行实际 DOM 操作的阶段,包括创建、更新或删除 DOM 元素等。使用深度优先遍历的方式,逐个处理 Fiber 树中的节点,根据协调阶段生成的更新计划,执行相应的 DOM 操作。

  • Layout (布局阶段):

    用于处理布局相关的任务,进行一些布局的优化,比如批量更新布局信息,减少浏览器的重排(reflow)次数,提高性能。其目标是最小化浏览器对 DOM 的重新计算布局,从而提高渲染性能。

1. 实现 commitWork

首先,在 react-reconciler/src/workLoop.tsrenderRoot 函数中,执行 commitRoot 函数。

  • commitRoot 是开始提交阶段的入口函数,调用 commitWork 函数进行实际的 DOM 操作;
  • commitWork 函数是提交阶段的核心,它会判断根节点是否存在上述 3 个阶段需要执行的操作,并执行实际的 DOM 操作,并完成 Fiber 树的切换。

我们先只实现 Mutation 阶段的功能,目前已支持的 DOM 操作有:Placement | Update | ChildDeletion,判断根节点的 flagssubtreeFlags 中是否包含这三个操作,如果有,则调用 commitMutationEffects 函数执行实际的 DOM 操作。

需要注意的是,由于 current 是与视图中真实 UI 对应的 Fiber 树,而 workInProgress 是触发更新后正在 Reconciler 中计算的 Fiber 树,因此在 DOM 操作执行完之后,需要将 current 指向 workInProgress,完成 Fiber 树的切换。

// packages/react-reconciler/src/workLoop.ts
import { MutationMask, NoFlags } from './fiberFlags';
import { commitMutationEffects } from './commitWork';
// ...

function renderRoot(root: FiberRootNode) {
    // 初始化 workInProgress 变量
    prepareFreshStack(root);
    do {
        try {
            // 深度优先遍历
            workLoop();
            break;
        } catch (e) {
            console.warn('workLoop发生错误:', e);
            workInProgress = null;
        }
    } while (true);

    // 创建根 Fiber 树的 Root Fiber
    const finishedWork = root.current.alternate;
    root.finishedWork = finishedWork;

    // 提交阶段的入口函数
    commitRoot(root);
}

function commitRoot(root: FiberRootNode) {
    const finishedWork = root.finishedWork;
    if (finishedWork === null) {
        return;
    }

    if (__DEV__) {
        console.log('commit 阶段开始');
    }

    // 重置
    root.finishedWork = null;

    // 判断是否存在 3 个子阶段需要执行的操作
    const subtreeHasEffects =
        (finishedWork.subtreeFlags & MutationMask) !== NoFlags;
    const rootHasEffects = (finishedWork.flags & MutationMask) !== NoFlags;

    if (subtreeHasEffects || rootHasEffects) {
        // TODO: BeforeMutation

        // Mutation
        commitMutationEffects(finishedWork);
        // Fiber 树切换,workInProgress 变成 current
        root.current = finishedWork;

        // TODO: Layout
    } else {
        root.current = finishedWork;
    }
}

2. 实现 Mutation

接下来我们来实现 Mutation 阶段执行 DOM 操作的具体实现,新建 packages/react-reconciler/src/commitWork.ts 文件,定义 commitMutationEffects 函数。

commitMutationEffects 函数负责深度优先遍历 Fiber 树,递归地向下寻找子节点是否存在 Mutation 阶段需要执行的 flags,如果遍历到某个节点,其所有子节点都不存在 flags(即 subtreeFlags == NoFlags),则停止向下,调用 commitMutationEffectsOnFiber 处理该节点的 flags,并且开始遍历其兄弟节点和父节点。

commitMutationEffectsOnFiber 会根据每个节点的 flags 和更新计划中的信息执行相应的 DOM 操作。

Placement 为例:如果 Fiber 节点的标志中包含 Placement,表示需要在 DOM 中插入新元素,此时就需要取到该 Fiber 节点对应的 DOM,并将其插入对应的父 DOM 节点中。

// packages/react-reconciler/src/commitWork.ts
import { Container, appendChildToContainer } from 'hostConfig';
import { FiberNode, FiberRootNode } from './fiber';
import {
    ChildDeletion,
    MutationMask,
    NoFlags,
    Placement,
    Update
} from './fiberFlags';
import { HostComponent, HostRoot, HostText } from './workTags';

let nextEffect: FiberNode | null = null;

export const commitMutationEffects = (finishedWork: FiberNode) => {
    nextEffect = finishedWork;

    // 深度优先遍历 Fiber 树,寻找更新 flags
    while (nextEffect !== null) {
        // 向下遍历
        const child: FiberNode | null = nextEffect.child;
        if (
            (nextEffect.subtreeFlags & MutationMask) !== NoFlags &&
            child !== null
        ) {
            // 子节点存在 mutation 阶段需要执行的 flags
            nextEffect = child;
        } else {
            // 子节点不存在 mutation 阶段需要执行的 flags 或没有子节点
            // 向上遍历
            up: while (nextEffect !== null) {
                // 处理 flags
                commitMutationEffectsOnFiber(nextEffect);

                const sibling: FiberNode | null = nextEffect.sibling;
                // 遍历兄弟节点
                if (sibling !== null) {
                    nextEffect = sibling;
                    break up;
                }
                // 遍历父节点
                nextEffect = nextEffect.return;
            }
        }
    }
};

const commitMutationEffectsOnFiber = (finishedWork: FiberNode) => {
    const flags = finishedWork.flags;
    if ((flags & Placement) !== NoFlags) {
        commitPlacement(finishedWork);
        finishedWork.flags &= ~Placement;
    }
    if ((flags & Update) !== NoFlags) {
        // TODO Update
        finishedWork.flags &= ~Update;
    }
    if ((flags & ChildDeletion) !== NoFlags) {
        // TODO ChildDeletion
        finishedWork.flags &= ~ChildDeletion;
    }
};

// 执行 DOM 插入操作,将 FiberNode 对应的 DOM 插入 parent DOM 中
const commitPlacement = (finishedWork: FiberNode) => {
    if (__DEV__) {
        console.log('执行 Placement 操作', finishedWork);
    }
    const hostParent = getHostParent(finishedWork);
    if (hostParent !== null) {
        appendPlacementNodeIntoContainer(finishedWork, hostParent);
    }
};

// 获取 parent DOM
const getHostParent = (fiber: FiberNode): Container | null => {
    let parent = fiber.return;
    while (parent !== null) {
        const parentTag = parent.tag;
        // 处理 Root 节点
        if (parentTag === HostRoot) {
            return (parent.stateNode as FiberRootNode).container;
        }
        // 处理原生 DOM 元素节点
        if (parentTag === HostComponent) {
            return parent.stateNode as Container;
        } else {
            parent = parent.return;
        }
    }
    if (__DEV__) {
        console.warn('未找到 host parent', fiber);
    }
    return null;
};

const appendPlacementNodeIntoContainer = (
    finishedWork: FiberNode,
    hostParent: Container
) => {
    if (finishedWork.tag === HostComponent || finishedWork.tag === HostText) {
        appendChildToContainer(finishedWork.stateNode, hostParent);
    } else {
        const child = finishedWork.child;
        if (child !== null) {
            appendPlacementNodeIntoContainer(child, hostParent);
            let sibling = child.sibling;
            while (sibling !== null) {
                appendPlacementNodeIntoContainer(sibling, hostParent);
                sibling = sibling.sibling;
            }
        }
    }
};

至此,我们就完成了 React 更新流程中的提交阶段(Commit Phase),实现了 DOM 树更新,下一节我们将实现 react-dom 包,跑通整个 React 首屏渲染流程。

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


《自己动手写 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 粉丝

腾讯新闻前端团队