《深入浅出react》总结之 11.2.1 Hooks 原理之 Hooks 与 fiber

91 阅读7分钟

Hooks 是什么?Hooks 是函数组件对应的 fiber 之间的沟通桥梁

关键点分析:

  1. 函数组件没有实例:类组件通过this访问实例属性(state、context等),而函数组件每次渲染都是重新执行函数,没有持久化实例。

  2. 状态持久化需求:为了在多次渲染间维持状态(如useState的值),需要将状态存储在某个与组件关联的持久化存储中。

  3. fiber作为存储载体:React为每个组件(包括函数组件)创建了一个fiber节点,该节点在组件的整个生命周期中存在。因此,fiber成为存储函数组件状态的理想位置。

  4. Hooks作为桥梁:Hooks(如useState、useEffect)提供了API,让函数组件能够从fiber中读取状态,并将更新后的状态写回fiber。

为什么说Hooks是桥梁?

  • 写操作:当函数组件调用Hook(如useState返回的setter函数)时,实际上是在更新fiber上存储的状态。

  • 读操作:当函数组件执行时,调用useState会从fiber的Hook链表中读取当前状态值。

总结:

Hooks作为函数组件和fiber之间的桥梁,使得函数组件能够:

  1. 持久化状态:将状态存储在fiber上,确保在多次渲染间保持状态。

  2. 访问状态:在函数组件执行过程中,通过Hooks从fiber中读取当前状态。

  3. 更新状态:通过Hook返回的更新函数(如setState)修改fiber上的状态,并触发重新渲染。

这种机制使得函数组件在无实例的情况下,依然能够拥有状态管理、生命周期等能力,同时与React的fiber架构深度集成。

所以下次再有人问我 hooks 是什么,我会说它是fiber 和 函数组件之间的桥梁,它能让函数组件同样拥有状态的持久化以及关键生命周期的能力

Hooks的调用环境

三种不同的Dispatcher对象来管理Hooks的调用

React通过三种不同的Dispatcher对象来管理Hooks的调用:

  1. ContextOnlyDispatcher:用于在函数组件外部调用Hooks时,此时调用任何Hook都会抛出错误。

  2. HooksDispatcherOnMount:在函数组件首次渲染(mount)时使用,负责初始化Hooks,建立Hooks与fiber的联系。

下面我们来详细解释:

步骤1:在React渲染函数组件之前,会设置当前的Dispatcher。

  • 如果是首次渲染,设置为HooksDispatcherOnMount。

  • 如果是更新渲染,设置为HooksDispatcherOnUpdate。

步骤2:在函数组件执行时,调用useState等Hook,实际上是从当前Dispatcher中获取对应的实现函数(如mountState或updateState)。

步骤3:当函数组件执行完毕后,React会将Dispatcher重置为ContextOnlyDispatcher(或者null,或者先前的Dispatcher,但确保不在组件内时不会使用Mount或Update的Dispatcher)。

这样,在函数组件外部调用Hook时,当前的Dispatcher就是ContextOnlyDispatcher,它上面的每个Hook都是抛出错误的函数(throwInvalidHookError)。 3. HooksDispatcherOnUpdate:在函数组件更新(update)时使用,负责更新状态。

关键点解释:

  • React通过一个全局的变量(通常称为currentDispatcher)来记录当前应该使用哪一种Dispatcher。
  • 当函数组件执行时,React会设置这个全局变量为正确的Dispatcher(Mount或Update),然后在函数组件内部调用Hook(如useState)时,实际上调用的是当前Dispatcher对应的实现(mountState或updateState)。
  • 而在函数组件外部,由于没有设置这个Dispatcher,就会使用ContextOnlyDispatcher(即默认的Dispatcher,或者是在非函数组件执行上下文中设置的Dispatcher),从而导致调用Hook时抛出错误。

详细解释:

下面我们来详细解释:

步骤1:在React渲染函数组件之前,会设置当前的Dispatcher。

  • 如果是首次渲染,设置为HooksDispatcherOnMount。

  • 如果是更新渲染,设置为HooksDispatcherOnUpdate。

步骤2:在函数组件执行时,调用useState等Hook,实际上是从当前Dispatcher中获取对应的实现函数(如mountState或updateState)。

步骤3:当函数组件执行完毕后,React会将Dispatcher重置为ContextOnlyDispatcher(或者null,或者先前的Dispatcher,但确保不在组件内时不会使用Mount或Update的Dispatcher)。

这样,在函数组件外部调用Hook时,当前的Dispatcher就是ContextOnlyDispatcher,它上面的每个Hook都是抛出错误的函数(throwInvalidHookError)。

// 定义三种Dispatcher
const ContextOnlyDispatcher = {
    useState: throwInvalidHookError,
    useEffect: throwInvalidHookError,
    // ... 其他Hook
};

const HooksDispatcherOnMount = {
    useState: mountState,
    useEffect: mountEffect,
    // ... 其他Hook
};

const HooksDispatcherOnUpdate = {
    useState: updateState,
    useEffect: updateEffect,
    // ... 其他Hook
};

// 当前使用的Dispatcher,默认为ContextOnlyDispatcher(报错模式)
let currentDispatcher = ContextOnlyDispatcher;

// 暴露给开发者使用的Hook(如useState)
export function useState(initialState) {
    // 实际上调用的是当前Dispatcher的useState
    return currentDispatcher.useState(initialState);
}

// 在渲染函数组件时,React会设置currentDispatcher
function renderFunctionalComponent(Component) {
    // 根据是mount还是update设置不同的Dispatcher
    if (isMount) {
        currentDispatcher = HooksDispatcherOnMount;
    } else {
        currentDispatcher = HooksDispatcherOnUpdate;
    }

    // 执行函数组件
    const children = Component(props);

    // 执行完毕后,将dispatcher重置为报错模式,防止在函数组件外部调用
    currentDispatcher = ContextOnlyDispatcher;

    return children;
}

设计模式角度分析

这种设计模式是策略模式(Strategy Pattern)代理模式(Proxy Pattern) 的结合,但更准确地说是策略模式的一种应用。

1. 策略模式(Strategy Pattern):

  • 定义:定义一系列算法,将每个算法封装起来,并使它们可以互相替换。策略模式让算法的变化独立于使用它的客户。

  • 应用:在React Hooks中,useStateuseEffect等Hook在mount阶段和update阶段有不同的实现(即不同的策略)。通过切换currentDispatcher,就相当于切换了策略:

  • HooksDispatcherOnMount:挂载阶段的策略(调用mountState等)

  • HooksDispatcherOnUpdate:更新阶段的策略(调用updateState等)

  • 优势将mount和update的逻辑分离,使代码更清晰,同时便于扩展(例如未来可能增加其他策略)。

2. 代理模式(Proxy Pattern)的体现:

核心思想:为其他对象提供一种代理以控制对这个对象的访问。

  • 开发者调用的 useState 不直接实现功能,而是代理到当前设置的 dispatcher

  • currentDispatcher 作为代理对象,控制实际调用的具体实现

  • 在函数组件外部调用Hook时,currentDispatcher被设置为ContextOnlyDispatcher,此时它充当了一个“保护代理”(Protection Proxy)的角色,阻止在非法位置调用Hook(通过抛出错误)。

  • 当进入renderFunctionalComponent函数时,代理被切换为具体的策略(mount或update策略)。

3. 代码流程分析:

  • 默认情况currentDispatcher = ContextOnlyDispatcher(所有Hook调用都会报错)。

  • 渲染函数组件时

  1. 根据isMount标志选择HooksDispatcherOnMountHooksDispatcherOnUpdate

  2. 设置currentDispatcher为对应的Dispatcher。

  3. 执行函数组件Component(props),此时内部调用的useState等Hook会从currentDispatcher获取当前策略。

  4. 执行完毕后重置currentDispatcherContextOnlyDispatcher,确保后续在非组件内部调用Hook会报错。

4. 设计目的:

  • 隔离阶段逻辑mount阶段(初始化)和update阶段(更新)的Hook处理逻辑不同,通过策略模式分离。

  • 防止错误调用利用代理模式在非组件渲染时禁止调用Hook(避免违反Hooks规则)。

  • 可扩展性如果需要加入其他阶段(如unmount),只需新增策略并修改设置逻辑。

总结:

这种设计模式主要是策略模式,通过动态切换Dispatcher对象来改变Hook的具体实现策略,同时结合了对Hook调用环境的保护(代理模式的思想)。

这种模式使得React可以在不同阶段(mount/update)使用不同的内部实现,同时确保Hooks在正确的上下文中被调用。

最后的总结:

其实我想说,这可能就是 了解 底层实现的最直接受益之一,我们看到优秀的开发者在处理复杂逻辑时用到的设计模式和处理方式,很有可能会被我们用到后续开发中,这就是成长吧!