随笔之为什么 hooks 不能出现在 if 语句里

564 阅读4分钟

这是最近我在面试中遇到的一个问题,记录一下我的思路和分析过程。

React 的基本渲染机制

在解答这个问题之前,首先要深入理解 React 的渲染机制,特别是它如何高效地管理组件的渲染和更新。

首次渲染

  1. 初始化组件的渲染

    React 会通过调用组件的 render 方法(对于函数组件,直接执行组件函数)来初始化组件的渲染过程。

  2. 将组件转化为虚拟 DOM (VNode)

    组件的返回值将被 React 转换成一个虚拟 DOM(VNode),这个 VNode 用于描述组件的结构和内容。

  3. 将 VNode 转化为 Fiber 对象

    VNode 随后会被转换为 Fiber 对象。Fiber 是 React 内部的一个重要数据结构,它用于描述组件的当前状态、更新信息等,并帮助 React 管理渲染和更新的流程。

  4. 形成 Fiber 树

    所有的 Fiber 对象会被组织成一棵树状结构,这棵树便是整个应用的组件树。这个 Fiber 树不仅仅是一个视觉上的结构,它还包含了 React 在渲染和更新过程中所需的所有信息。

更新组件

  1. 双缓存树技术

    在组件更新时,React 会利用 双缓存树 技术,生成一个新的 Fiber 树,并与当前的 Fiber 树进行比较。这个过程允许 React 高效地进行增量更新。

  2. Diff 算法(协调阶段)

    React 会比较新旧 Fiber 树,找出组件差异,进而确定哪些组件需要更新、删除或新增。这个过程是非常高效的,React 会通过浅比较来快速识别发生变化的部分,从而最小化渲染的开销。

  3. 标记更新

    被标记为需要更新的 Fiber 节点将在下一次渲染时被更新,React 会根据更新的 Fiber 树来更新实际的 DOM 结构。

  4. 应用更新后的状态

    最终,经过比较和更新的新的 Fiber 树将会替换掉旧的树,React 会根据新的树来渲染更新后的界面。


为什么 hooks 不能出现在 if 语句里?

在 React 渲染和更新的过程中,我们如何管理 hooks 呢?为了理解这一点,首先需要明确,每个组件都会有自己的一组 hooks,这些 hooks 会被存放在对应组件的 Fiber 对象中。

我们可以把这些信息结构化,简化成以下形式:

const FiberNode = {
    tag: null,               // 节点类型,例如: HostComponent, FunctionComponent, ClassComponent 等
    key: null,               // 唯一标识符
    stateNode: null,         // 组件实例或 DOM 节点
    elementType: null,       // 节点对应的元素类型
    return: null,            // 指向父节点
    child: null,             // 指向第一个子节点
    sibling: null,           // 指向下一个兄弟节点
    index: 0,                // 在兄弟节点中的索引位置

    memoizedState: {
        baseQueue: null,
        baseState: 'hook1',
        memoizedState: null,
        queue: null,
        next: {
            baseQueue: null,
            baseState: null,
            memoizedState: 'hook2',
            next: null,
            queue: null
        }
    },     // 存储 hooks 信息

    effectTag: 0,            // 标记该节点的副作用(例如: Placement, Update, Deletion 等)
    nextEffect: null,        // 指向下一个要处理的副作用节点
};

可以看到,memoizedState 是用于存储 hooks 的地方,它并不是一个数组,而是一个链表结构。每个 hook 都通过 next 指针连接到下一个 hook

为什么顺序如此重要?

React 在每次渲染时,都会按照固定的顺序依次访问和执行这些 hooks。这对于确保每个 hook 都能正确地管理它的状态至关重要。每次渲染,React 都会从头到尾遍历整个 memoizedState 链表,基于新的渲染结果更新每个 hook 的状态。

关键问题:hook 调用顺序

如果我们在条件语句(如 if)中调用 hook,那么 在不同的渲染周期中,hook 的调用顺序可能会发生变化。React 是按照 memoizedState 中的顺序来执行每个 hook 的,但如果这个顺序发生了改变,React 就无法保证每个 hook 会正确地保持自己的状态。这会导致无法正确更新 hook 的状态,从而出现渲染错误。

总结

为了保证 React 在每次渲染时能够正确地访问和更新 hook,React 强制要求在 所有渲染过程中,hook 的调用顺序必须保持一致。如果 hook 的调用顺序发生变化,React 将无法正确地进行状态更新,可能会导致渲染错误。因此,不应该在条件语句中调用 hook,因为它会打破这种顺序的一致性