跳到主要内容

React 并不会通过“变量名”来记住组件的状态,而是通过“调用顺序”和“链表结构”来对应的。

核心在于其独特的数据结构和执行规则,使得函数组件能够“记住”状态和副作用。

    1. 数据结构 :每个组件实例都关联着一个链表,用户储存所有的 Hooks 的状态
    1. 执行规则 : Hooks 必须按照固定的顺序,以此来确保状态与 HOOK 的一一对应

一、核心结构

React 内部,每个函数都对应一个 Fiber 节点 。这个节点(可以看作是组件的“骨架”或实例信息)上有一个 memoizedState 属性,它指向一个单向链表 ,这个链表就是储存所有 Hooks 状态的地方。

链表中的每一个节点(一个 HOOK 对象)都包含一下关键信息:

  • memoizedState :储存当前 Hook 的状态值(例如 useState 的值、 useEffect 的回调函数和依赖 )
  • next :指向下一个 Hook 节点的指针,将所有 Hooks 串联起来
  • queue :储存该 Hook 的更新列队(例如 useStatesetState 触发的更新)

Hook 对象的结构大致如下:

Hook 调用 mountWorkInProgressHook 初始化
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null, // 当前状态值(对于 useState ) 或依赖项(对于 useEffect)

baseState: null, // 基础状态(用于计算更新)
baseQueue: null, // 原始
queue: null, // 更新列队(储存 dispatchAction 产生的 update)

next: null, // 指向下一个 Hook 的指针(关键!)
};

if (workInProgressHook === null) {
// This is the first hook in the list
// 这是列表中的第一个钩子
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
// Append to the end of the list
// 添加到列表末尾
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
为什么是链表?

链表允许 React 在组件渲染时,通过简单的指针移动( currentHook = current.next )来按顺序访问状态,而不需要像数组那样在中间插入元素时进行大量的索引移动。

1. 状态是怎么被记住的

这个过程分为两个阶段:

    1. 首次渲染 :当组件首次渲染时, React 会从上到下执行函数组件的代码。每当遇到一个 Hook (如 useState ), React 就会创建(通过 mountWorkInProgressHook 方法)一个新的 Hook 节点,将其添加到 Fiber 节点的 Hook 链表莫问。并初始化它的 memoizedState
    1. 更新重渲染 :当组件状态改变时触发重新渲染时,函数组件会再次执行。此时, React 不会再创建新的 Hook 节点,而是按照与首次渲染完全相同的顺序,遍历已经存在的 Hook 链表,依次读取每个节点储存的 memoizedState

二、为什么 Hook 不能写在条件语句中

根源就在于链表结构顺序依赖

React 依靠 Hook 的调用顺序来确保每次渲染时,其状态能正确对应到具体的 Hook 上。

一旦顺序发生改变, React 就无法正确匹配状态和 Hook ,导致数据错乱或崩溃。

1. 以 useState 为例

useState 的工作原理完美展示了 React 的 Hook 机制:

    1. 读取状态 :当调用 useState(initialValue) 时, React 会根据当前的调用顺序,在 HOOK 链表中构建一个新的 Hook 并添加状态到 hook.memoizedStatehook.baseState
    初始化 useState 实现方法
    function mountState<S>(
    initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
    const hook = mountStateImpl(initialState);
    const queue = hook.queue;
    const dispatch: Dispatch<BasicStateAction<S>> = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
    ) as any;
    queue.dispatch = dispatch;
    return [hook.memoizedState, dispatch];
    }
    function mountStateImpl<S>(initialState: (() => S) | S): Hook {
    const hook = mountWorkInProgressHook();
    if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    initialState = initialStateInitializer();
    if (shouldDoubleInvokeUserFnsInHooksDEV) {
    setIsStrictModeForDevtools(true);
    try {
    initialStateInitializer();
    } finally {
    setIsStrictModeForDevtools(false);
    }
    }
    }
    hook.memoizedState = hook.baseState = initialState;
    const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState as any,
    };
    hook.queue = queue;
    return hook;
    }
    1. 更新状态 :当调用 setState(newValue) 时, React 并不会立即修改那个状态。它会将这个更新请求放入对应的 Hook 节点的 queue(更新列队)中,然后出发组件的重新渲染
    1. 处理更新 :在重新渲染阶段, React 会处理 queue 中的所有更新,计算出最新的 memoizedState 值,并在下一次渲染时返回

三、惰性初始 State(Lazy Initial State)

当使用 useState(() => expensiveComputation()) 时,React 并不会在每次渲染都执行这个函数。

实现逻辑

mount 阶段,useState 会检查传入的参数。

    1. 如果参数时一个函数(惰性初始化), React 会立即执行它一次,得到初始值
    1. 将这个值存入 Hook 对象的 memoizedStatebaseState
    1. 关键点 : 在后续的更新( Update )阶段, React 不再关心初始值,而是直接读取 memoizedState 或基于 baseState 计算新值

四、dispatchAction 与更新列队

当调用 setState (即 dispatchAction )时,React 并没有立即修改状态,而是采用了“入队”的策略。

dispatchAction 的工作流程:

    1. 创建 Update 对象 :包含要更新的动作( action )、优先级( lane )等信息
    1. 插入循环链表 :每个 Hook 都有一个 queue ,它是一个循环链表dispatchAction 会将新的 Update 对象插入到这个链表中
    • 结构类似于: Update1 ➞ Update2 ➞ Update3 ➞ (回到头)
    1. 调度更新 :触发 React 的调度机制,安排组件重新渲染

在渲染阶段( Update Reducer ) : React 会遍历这个 queue 链表,依次计算所有的更新( reducer ),计算出最新的 memoizedState ,然后返回

五、 effectTag 标记(插入/更新/删除)

虽然 Hooks 本身是链表结构,但 React 如果知道该对该副作用( Effect )做什么操作(增删改)呢?这依赖于 Fiber 节点的 flags (旧版本称为 effectTag )。

在 Reconciliation (调和)阶段, React 会对比新旧 Hooks 链表:

  • 插入( Placement ) :如果是 mount 阶段,或者在自定义 Hook 中动态增加了 Hook (虽然不推荐), React 会给 Fiber 打上 Placement 标记
  • 更新( Update ) :如果 Hook 的依赖项 ( deps )发生变化(通过 Object.is 对比),或者没有 deps 数组, React 会标记该 Effect 需要重新执行
  • 删除( Deletion ) :这通常发生在组件卸载时。 React 会遍历 Fiber 树,找到带有副作用标记的节点,执行清理函数( destroy )