重新理解 React Hooks 原理

952 阅读8分钟

我正在参加「掘金·启航计划」

React Hooks 是 React 16.8 中引入的新特性,它允许我们在函数组件中使用 state 和其他 React 特性。在本文中,我们将深入了解 React Hooks 的实现原理。

useState Hook 的实现原理

useState Hook 允许我们在函数组件中使用 state。当我们调用 useState Hook 时,它会返回一个数组,其中包含当前状态值和一个更新状态值的函数。这个函数可以接受一个新的状态值,并将其设置为组件的新状态。

useState Hook 的实现原理是基于 React 的 Fiber 架构。每个组件都有一个对应的 Fiber 对象,该对象包含了组件的状态和其他相关信息。当我们调用 useState Hook 时,React 会创建一个新的 Fiber 节点,并将其添加到组件的 Fiber 树中。

在 Fiber 节点中,useState Hook 会创建一个新的状态值,并将其存储在 Fiber 节点的 state 字段中。当我们调用更新状态值的函数时,React 会创建一个新的 Fiber 节点,并将其添加到更新队列中。在执行下一次渲染时,React 会遍历更新队列,并根据队列中的操作更新组件的状态。

需要注意的是,useState Hook 的更新是异步的。这意味着调用更新状态值的函数并不会立即更新组件的状态,而是将更新操作添加到更新队列中。这样可以优化性能,避免不必要的重复渲染。

useState Hook 的实现源码位于 React 源码的 ReactHooks.js 文件中。当我们调用 useState Hook 时,实际上是调用了 useStateImpl 函数。该函数接受一个初始状态值,并返回一个包含当前状态值和更新状态值的函数的数组。

function useStateImpl<S>(initialState: (() => S) | S): [S, Dispatch<SetStateAction<S>>] {
  // 获取当前的 Fiber 节点
  const fiber = getCurrentlyRenderingFiber();
  // 获取当前的 Hooks 链表
  const hook = updateWorkInProgressHook();
  // 如果 Hooks 链表中没有对应的 Hook,则创建一个新的 Hook 节点
  if (!fiber.alternate) {
    // 创建一个新的 Hook 节点,并将其添加到 Hooks 链表中
    const state: HookState<S> = {
      queue: null,
      baseState: initialState,
      baseUpdate: null,
      next: null,
    };
    hook.memoizedState = state;
    // 将 Hooks 链表中的 first 和 last 指针指向新创建的 Hook 节点
    if (!fiber.memoizedState) {
      fiber.memoizedState = hook.memoizedState;
    } else {
      fiber.memoizedState.lastHook.next = hook.memoizedState;
    }
    fiber.memoizedState.lastHook = hook.memoizedState;
  } else {
    // 如果 Hooks 链表中已经有对应的 Hook,则直接使用该 Hook 节点
    const oldHook = fiber.alternate.memoizedState.lastHook;
    hook.memoizedState = oldHook.next;
    // 如果 Hook 节点的初始状态值不同,则创建一个新的 Hook 节点
    if (Object.is(oldHook.memoizedState.baseState, initialState)) {
      hook.memoizedState.baseState = initialState;
    } else {
      const state: HookState<S> = {
        queue: null,
        baseState: initialState,
        baseUpdate: null,
        next: null,
      };
      hook.memoizedState = state;
      oldHook.next = state;
      fiber.alternate.memoizedState.lastHook = state;
    }
  }
  // 返回当前状态值和更新状态值的函数的数组
  const queue = hook.memoizedState.queue;
  const dispatch: Dispatch<SetStateAction<S>> = queue ? dispatchAction.bind(null, fiber, queue) : dispatchAction.bind(null, fiber, hook.memoizedState);
  return [hook.memoizedState.baseState, dispatch];
}

dispatchAction 方法主要是用于更新组件状态的内部方法。它的具体实现如下:

function dispatchAction(fiber, queue, action) {
  const alternate = fiber.alternate;
  if (fiber === currentlyRenderingFiber || alternate !== null && alternate === currentlyRenderingFiber) {
    // 如果当前正在渲染该 Fiber 节点或其备选节点,则将更新推入 pending 嵌套更新队列中
    didScheduleRenderPhaseUpdate = true;
    const update = {
      expirationTime: renderExpirationTime,
      suspenseConfig: null,
      action,
      eagerReducer: null,
      eagerState: null,
      next: null,
    };
    if (queue.last === null) {
      // 如果队列为空,则将该 update 设置为队列的第一个更新
      queue.first = queue.last = update;
    } else {
      // 否则将该 update 加入队列的末尾
      queue.last.next = update;
      queue.last = update;
    }
    if (fiber.expirationTime === NoWork) {
      // 如果 Fiber 上没有过期时间,则将其标记为当前渲染的过期时间
      fiber.expirationTime = renderExpirationTime;
    }
    return;
  }

  flushPassiveEffects();
  let currentTime = requestCurrentTime();
  let expirationTime = computeExpirationForFiber(currentTime, fiber);

  const update = {
    expirationTime,
    suspenseConfig: null,
    action,
    eagerReducer: null,
    eagerState: null,
    next: null,
  };

  insertUpdateIntoFiber(fiber, update);
  scheduleWork(fiber, expirationTime);
}

该方法接受三个参数:

  • fiber:表示当前需要更新状态的 Fiber 节点
  • queue:表示该 Fiber 节点所对应的更新队列
  • action:表示需要执行的状态更新操作

具体实现中,dispatchAction 方法会首先判断当前是否正在渲染该 Fiber 节点或其备选节点,如果是,则将更新推入 pending 嵌套更新队列中,以便在该组件渲染结束后执行更新。

如果不是,则会刷新被动更新,然后计算该 Fiber 何时需要重新渲染,并将更新插入 Fiber 节点所对应的更新队列中,最终调用 scheduleWork 方法来安排重新渲染。

useEffect Hook 的实现原理

useEffect Hook 允许我们在函数组件中执行副作用操作,例如订阅事件、发送网络请求等。当我们调用 useEffect Hook 时,它会接受一个回调函数,并在组件渲染完成后执行该函数。

useEffect Hook 的实现原理也是基于 React 的 Fiber 架构。当我们调用 useEffect Hook 时,React 会创建一个新的 Fiber 节点,并将其添加到组件的 Fiber 树中。在 Fiber 节点中,useEffect Hook 会将回调函数存储在 effect 字段中。

在执行下一次渲染时,React 会遍历 Fiber 树,并执行每个节点的 effect 字段中存储的回调函数。当组件将要被卸载时,React 也会执行 effect 字段中存储的清除函数,以清除副作用操作。

需要注意的是,useEffect Hook 中的回调函数和清除函数都是可选的。如果没有提供清除函数,则 React 会自动清除副作用操作。

useEffect Hook 的实现源码位于 React 源码的 ReactFiberHooks.js 文件中。当我们调用 useEffect Hook 时,实际上是调用了 mountEffectImpl 或 updateEffectImpl 函数。这两个函数分别用于组件挂载和更新时执行副作用操作。

useEffect Hook 的实现原理主要是基于 React Fiber 架构中的 Effect 链表和调度器实现的。

当组件渲染时,React 会创建一个 Effect 链表,用于存储组件中所有的副作用操作。每个 Effect 节点包含了副作用操作的类型、依赖项数组、回调函数等信息。在组件更新时,React 会根据 Effect 链表中的节点来执行副作用操作。

以下是 useEffect Hook 的源码实现:

function updateEffectImpl(fiberFlags: Flags, hookFlags: HookFlags, create: () => mixed, deps: Array<mixed> | void | null): void {
  // 获取当前的 Fiber 节点和 Hooks 链表
  const fiber = getCurrentlyRenderingFiber();
  const hook = updateWorkInProgressHook();
  // 获取上一个 Effect 节点
  const lastEffect = hook.memoizedState;
  // 比较依赖数组是否发生变化
  if (depsAreEqual(lastEffect.deps, deps)) {
    // 如果依赖数组未发生变化,则跳过副作用操作
    hook.memoizedState = { ...lastEffect, create, deps };
    return;
  }
  // 如果依赖数组发生变化,则创建一个新的 Effect 节点,并将其添加到 Hooks 链表中
  const effectTag = fiberFlags & ShouldCapture ? (hookFlags & Passive ? Passive | PassiveStatic : PassiveStatic) : hookFlags & Passive ? Passive : NoFlags;
  const createTag = effectTag & (Update | Callback);
  pushEffect(hook, createTag, create, deps);
}

在 useEffect 中,我们首先获取当前正在渲染的 Fiber 节点和对应的 Hook。然后,我们比较传入的依赖项数组和上一次渲染时 Hook 中保存的依赖项数组是否相等。如果相等,则说明依赖项未发生变化,直接返回。否则,我们将传入的依赖项数组保存到 Hook 中,并执行传入的副作用操作函数。

在执行副作用操作函数后,我们会判断其返回值是否为一个清理函数。如果是,则将其添加到 Effect 链表中,并标记该节点的类型为 HookLayout | HookHasEffect,表示该节点包含有布局相关的副作用操作。

在组件更新时,React 会按照 Effect 链表中节点的顺序来执行副作用操作。对于类型为 HookLayout | HookHasEffect 的节点,React 会优先执行其清理函数,然后再执行副作用操作函数。这样可以确保在组件更新时正确地处理布局相关的副作用操作。

自定义 Hook 的实现原理

自定义 Hook 允许我们将逻辑重用,并使代码更加模块化和可读性更好。自定义 Hook 实际上是一个普通的 JavaScript 函数,它可以使用其他 Hook 和普通 JavaScript 函数来实现逻辑。

自定义 Hook 的实现原理非常简单。当我们定义一个自定义 Hook 时,实际上就是定义了一个普通的 JavaScript 函数,并在该函数中使用其他 Hook 和普通 JavaScript 函数来实现逻辑。当我们在组件中调用自定义 Hook 时,React 会创建一个新的 Fiber 节点,并将其添加到组件的 Fiber 树中。

需要注意的是,自定义 Hook 名称必须以 "use" 开头,这是 React 的约定。这样可以使代码更具可读性,并且能够让 React 在编译时检测到错误。

自定义 Hook 的实现原理基于 React Hooks 的设计,主要是通过将状态逻辑封装在函数中,并使用 useState、useEffect 等 Hooks 来实现状态管理和副作用操作。

以下是一个简单的自定义 Hook 示例,用于计算组件宽度:

import { useState, useEffect } from 'react';

function useComponentWidth() {
  const [width, setWidth] = useState(0);
  const ref = useRef(null);

  useEffect(() => {
    const handleResize = () => {
      if (ref.current) {
        setWidth(ref.current.offsetWidth);
      }
    };
    window.addEventListener('resize', handleResize);
    handleResize();
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);

  return { width, ref };
}

在这个 Hook 中,我们使用了 useState 和 useEffect 来管理组件的宽度和添加/移除窗口大小变化事件监听器。在每次窗口大小变化时,我们会更新组件的宽度,并返回宽度和一个用于获取组件引用的 ref 对象。

这个自定义 Hook 的实现原理与 React Hooks 的实现原理类似,都是通过在函数组件中使用 Hooks 来实现状态管理和副作用操作。因此,React Hooks 的设计使得自定义 Hook 变得非常简单和灵活,可以根据不同的需求来实现不同的自定义 Hook。