头图

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

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

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

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

1. React 更新流程

React 中的更新流程大致可以分为以下几个阶段:

  1. 触发更新(Update Trigger): 更新可以由组件的状态变化、属性变化、父组件的重新渲染、用户事件等触发,如:

    • 创建 React 应用的根对象 ReactDOM.creatRoot().render()
    • 类组件 this.setState()
    • 函数组件 useState useEffect
  2. 调度阶段(Schedule Phase): 调度器根据更新任务的优先级,将更新任务添加到相应的更新队列中,这个阶段决定了何时以及以何种优先级执行更新任务。
  3. 协调阶段(Reconciliation Phase): 也叫 Render 阶段, Reconciler 负责构建 Fiber 树,处理新旧虚拟 DOM 树之间的差异,生成更新计划,确定需要进行的操作。
  4. 提交阶段(Commit Phase): 提交阶段将更新同步到实际的 DOM 中,React 执行 DOM 操作,例如创建、更新或删除 DOM 元素,反映组件树的最新状态。

2. 实现 UpdateQueue

为了实现上述更新流程,首先需要定义两个数据结构:UpdateUpdateQueue,它们一起组成了 React 中管理组件状态更新的机制。

Update(更新)

  • Update 表示对组件状态的一次更新操作。
  • 当组件状态发生变化时(例如由 setState 触发),React 会创建一个 Update 对象,其中包含了新的状态或者状态的更新部分。
  • Update 对象描述了如何修改组件的状态,如添加新的状态、更新现有的状态、删除状态等,以及与此更新相关的一些元数据,如优先级等。
  • Update 对象记录了组件状态的变化,但实际的状态更新并不会立即执行,而是会被添加到更新队列中等待处理。

UpdateQueue(更新队列)

  • UpdateQueue 是一个队列数据结构,用于存储组件的更新操作。
  • 当组件的状态发生变化时,会生成一个新的 Update 对象,并将其添加到组件的 UpdateQueue 中。
  • UpdateQueue 管理着组件的状态更新顺序,确保更新操作能够按照正确的顺序和优先级被应用到组件上。
  • 更新队列通常是以链表的形式实现的,每个 Update 对象都链接到下一个更新对象,形成一个更新链表。在 React 的更新过程中,会遍历更新队列,并根据其中的更新操作来更新组件的状态以及更新 DOM。

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

// packages/react-reconciler/src/updateQueue.ts
import { Action } from 'shared/ReactTypes';
import { Update } from './fiberFlags';

// 定义 Update 数据结构
export interface Update<State> {
    action: Action<State>;
}

// 定义 UpdateQueue 数据结构
export interface UpdateQueue<State> {
    shared: {
        pending: Update<State> | null;
    };
}

// 创建 Update 实例的方法
export const createUpdate = <State>(action: Action<State>): Update<State> => {
    return {
        action
    };
};

// 创建 UpdateQueue 实例的方法
export const createUpdateQueue = <State>(): UpdateQueue<State> => {
    return {
        shared: {
            pending: null
        }
    };
};

// 将 Update 添加到 UpdateQueue 中的方法
export const enqueueUpdate = <State>(
    updateQueue: UpdateQueue<State>,
    update: Update<State>
) => {
    updateQueue.shared.pending = update;
};

// 从 UpdateQueue 中消费 Update 的方法
export const processUpdateQueue = <State>(
    baseState: State,
    pendingUpdate: Update<State> | null
): { memoizedState: State } => {
    const result: ReturnType<typeof processUpdateQueue<State>> = {
        memoizedState: baseState
    };
    if (pendingUpdate !== null) {
        const action = pendingUpdate.action;
        if (action instanceof Function) {
            // 若 action 是回调函数:(baseState = 1, update = (i) => 5i)) => memoizedState = 5
            result.memoizedState = action(baseState);
        } else {
            // 若 action 是状态值:(baseState = 1, update = 2) => memoizedState = 2
            result.memoizedState = action;
        }
    }
    return result;
};
// packages/shared/ReactTypes.ts
// ...

// 定义 Action type
export type Action<State> = State | ((prevState: State) => State);

3. 实现触发更新

上面我们提到,更新 React 应用可以由多种触发方式引发,包括组件的状态变化、属性变化、父组件的重新渲染以及用户事件等。

先来处理创建 React 应用的根对象这种情况,也就是 ReactDOM.createRoot(rootElement).render(<App/>) 这条语句:

  • ReactDOM.createRoot() 函数生成一个新的 Root 对象,它在源码中是 FiberRootNode 类型,充当了 React 应用的根节点。
  • rootElement 则是要渲染到的 DOM 节点,它在源码中是 hostRootFiber 类型,作为 React 应用的根 DOM 节点。
  • render() 方法将组件 <App/> 渲染到根节点上。在这个过程中,React 会创建一个代表 <App/> 组件的 FiberNode,并将其添加到 Root 对象的 Fiber 树上。

根据上图,我们先来实现 FiberRootNode 类型:

// packages/react-reconciler/src/fiber.ts
export class FiberRootNode {
    container: Container;
    current: FiberNode;
    finishedWork: FiberNode | null;
    constructor(container: Container, hostRootFiber: FiberNode) {
        this.container = container;
        this.current = hostRootFiber;
        // 将根节点的 stateNode 属性指向 FiberRootNode,用于表示整个 React 应用的根节点
        hostRootFiber.stateNode = this;
        // 指向更新完成之后的 hostRootFiber
        this.finishedWork = null;
    }
}

接着我们来实现 ReactDOM.createRoot().render() 过程中调用的 API:

  • createContainer 函数: 用于创建一个新的容器(container),该容器包含了 React 应用的根节点以及与之相关的一些配置信息。createContainer 函数会创建一个新的 Root 对象,该对象用于管理整个 React 应用的状态和更新。
  • updateContainer 函数: 用于更新已经存在的容器中的内容。在内部,updateContainer 函数会调用 scheduleUpdateOnFiber 等方法,通过 Fiber 架构中的协调更新过程,将新的 React 元素(element)渲染到容器中,并更新整个应用的状态。

新建文件 fiberReconciler.ts,里面有 createContainerupdateContainer 两个函数,在 workLoop.ts 文件中实现 scheduleUpdateOnFiber函数:

// packages/react-reconciler/src/fiberReconciler.ts
import { Container } from 'hostConfig';
import { FiberNode, FiberRootNode } from './fiber';
import { HostRoot } from './workTags';
import {
    UpdateQueue,
    createUpdate,
    createUpdateQueue,
    enqueueUpdate
} from './updateQueue';
import { ReactElementType } from 'shared/ReactTypes';
import { scheduleUpdateOnFiber } from './workLoop';

export function createContainer(container: Container) {
    const hostRootFiber = new FiberNode(HostRoot, {}, null);
    const root = new FiberRootNode(container, hostRootFiber);
    hostRootFiber.updateQueue = createUpdateQueue();
    return root;
}

export function updateContainer(
    element: ReactElementType | null,
    root: FiberRootNode
) {
    const hostRootFiber = root.current;
    const update = createUpdate<ReactElementType | null>(element);
    enqueueUpdate(
        hostRootFiber.updateQueue as UpdateQueue<ReactElementType | null>,
        update
    );
    scheduleUpdateOnFiber(hostRootFiber);
    return element;
}
// packages/react-reconciler/src/workLoop.ts
// ...

// 调度功能
export function scheduleUpdateOnFiber(fiber: FiberNode) {
    const root = markUpdateFromFiberToRoot(fiber);
    renderRoot(root);
}

// 从触发更新的节点向上遍历到 FiberRootNode
function markUpdateFromFiberToRoot(fiber: FiberNode) {
    let node = fiber;
    while (node.return !== null) {
        node = node.return;
    }
    if (node.tag == HostRoot) {
        return node.stateNode;
    }
    return null;
}

另外,在上一节中,我们在实现 prepareFreshStack 函数时,直接将 root 作为参数赋值给了 workInProgress,但现在我们知道了,root 其实是 FiberRootNode 类型的,不能直接赋值给 FiberNode 类型的 workInProgress,所以需要写一个 createWorkInProgress 函数处理一下:

// packages/react-reconciler/src/workLoop.ts
function renderRoot(root: FiberRootNode) {
    prepareFreshStack(root);
    do {
        try {
            workLoop();
            break;
        } catch (e) {
            console.warn('workLoop发生错误:', e);
            workInProgress = null;
        }
    } while (true);
}

// 初始化 workInProgress 变量
function prepareFreshStack(root: FiberRootNode) {
    workInProgress = createWorkInProgress(root.current, {});
}

// ...
// packages/react-reconciler/src/fiber.ts
// ...

// 根据 FiberRootNode.current 创建 workInProgress
export const createWorkInProgress = (
    current: FiberNode,
    pendingProps: Props
): FiberNode => {
    let workInProgress = current.alternate;
    if (workInProgress == null) {
        // 首屏渲染时(mount)
        workInProgress = new FiberNode(current.tag, pendingProps, current.key);
        workInProgress.stateNode = current.stateNode;

        // 双缓冲机制
        workInProgress.alternate = current;
        current.alternate = workInProgress;
    } else {
        // 非首屏渲染时(update)
        workInProgress.pendingProps = pendingProps;
        // 将 effect 链表重置为空,以便在更新过程中记录新的副作用
        workInProgress.flags = NoFlags;
        workInProgress.subtreeFlags = NoFlags;
    }
    // 复制当前节点的大部分属性
    workInProgress.type = current.type;
    workInProgress.updateQueue = current.updateQueue;
    workInProgress.child = current.child;
    workInProgress.memoizedProps = current.memoizedProps;
    workInProgress.memoizedState = current.memoizedState;

    return workInProgress;
};

至此,我们已经实现了 React 应用在首次渲染或后续更新时的大致更新流程,一起来回顾一下:

  • 首先,我们通过 createContainer 函数创建了 React 应用的根节点 FiberRootNode,并将其与 DOM 节点(hostFiberRoot)连接起来;
  • 然后,通过 updateContainer 函数创建了一个更新(update),并将其加入到更新队列(updateQueue)中,启动了首屏渲染或后续更新的机制;
  • 接着会调用 scheduleUpdateOnFiber 函数开始调度更新,从触发更新的节点开始向上遍历,直到达到根节点 FiberRootNode
  • 接着会调用 renderRoot 函数,初始化 workInProgress 变量,生成与 hostRootFiber 对应的 workInProgress hostRootFiber
  • 接着就开始 Reconciler 的更新流程,即 workLoop 函数,对 Fiber 树进行深度优先遍历(DFS);
  • 在向下遍历阶段会调用 beginWork 方法,在向上返回阶段会调用 completeWork 方法,这两个方法负责 Fiber 节点的创建、更新和处理,具体实现会在下一节会讲到。

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


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

腾讯新闻前端团队