【React】源码解析—Hooks的工作机制

377 阅读12分钟

但行好事 莫问前程

前言

出于以下动机

  • 简化组件:用函数替代类,避免 this 和生命周期混乱。
  • 逻辑复用:通过自定义 Hook 抽离重复逻辑(如数据请求、监听事件)。
  • 功能聚焦:每个 Hook 专注一个功能(如 useState 管状态,useEffect 管副作用)。

React 16.8 新增特性 —— Hook,让函数式组件也能使用 状态state 和 其他 react 特性。

本文代码基于v18.3.1,内容精确到每一个变量,每一行代码,一起从源码层面学习 hooks 的工作机制。

简介

在学习源码之前,你应该知道以下内容:

  • React 使用 双缓冲机制(Double Buffering)  实现 Fiber 树的更新
  • Hooks 以 链表形式 存储在 Fiber 的 memoizedState 属性中,每个 Hook 对应链表的一个节点
type Hook = {
  memoizedState: any,         // 当前状态(如 useState 的值、useEffect 的依赖、useRef的ref对象...)
  baseState: any,             // 基础状态(用于更新计算)
  baseQueue: Update<any, any> | null, // 待处理的更新队列
  queue: any,// 待更新队列(如 setState 的批量更新)、用于更新的dispatch函数、更新优先级Lanes等信息
  next: Hook | null,          // 指向下一个 Hook 节点,用于构成链表
};

type fiber = {
  memoizedState: Hook | null,  // Hooks 链表的头节点
  // ...其他 Fiber 字段(如 stateNode、alternate) 
}

然后认识一些重要的变量:

  • current: 指向 当前已渲染的 Fiber 树(旧,对应屏幕上显示的内容)。
  • workInProgress: 指向 正在构建的新 Fiber 树(新,内存中构建的更新版本)。
  • currentHook: 已渲染的 Fiber 树 中,正在处理的 Hook 节点。
  • workInProgressHook: 新的 Fiber 树 中,正在构建或更新的 Hook 节点。
  • currentlyRenderingFiber: 当前正在执行的函数组件对应的 Fiber 节点(即 workInProgress Fiber)。
  • ReactCurrentDispatcher: 调度器对象,根据当前渲染阶段(Mount or Update)分配不同的调度器。

函数组件的更新和渲染,会触发 updateFunctionComponent 函数,调用 renderWithHooks 函数 -> 执行 Component(props) 组件函数。

我们以 renderWithHooks 为入口,分析 Hook 是如何运行的。

renderWithHooks

react/packages/react-reconciler/src/ReactFiberHooks.new.js

抛开_DEV_相关的调试代码和一些注释,精简 renderWithHooks 函数:

export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes
): any {
  renderLanes = nextRenderLanes;
  // 1. 设置当前正在渲染的 Fiber 节点
  currentlyRenderingFiber = workInProgress;
  
  // 2. 重置 Hooks 链表指针(初次渲染或更新)
  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  
  // 3. 区分 Mount 和 Update 阶段,选择 Hooks 实现
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;
      
  // 4. 执行组件函数(内部会按顺序调用 Hooks)
  let children = Component(props, secondArg);
  
  // 5. 重置全局变量,准备下一轮渲染
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  
  renderLanes = NoLanes;
  currentlyRenderingFiber = (null: any);

  currentHook = null;
  workInProgressHook = null;

  return children;
}

解读

分块解读上述代码:

  1. 设置当前正在渲染的 Fiber 节点
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
  • renderLanes = nextRenderLanes;

设置渲染的优先级,在 React16 的早期版本中,官方采用 expirationTime 模型来调度渲染的优先级,在 React17+ 等版本中被 Lane 模型所替代。

expirationTime 模型仅通过时间设置优先级,存在灵活性差、优先级冲突(如用户的输入可能无法得到优先处理)、优先级单一等问题。

  • currentlyRenderingFiber = workInProgress;

如前文所说,通过 currentlyRenderingFiber 变量,可以正确从全局获取到 当前正在执行的函数组件对应的 Fiber 节点(即 workInProgress Fiber)。

  1. 重置 workInProgress fiber 状态
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;

置空 workInProgress 的 Hooks链表、更新队列、渲染优先级,确保每次渲染都是纯净的。

后续 React 会根据函数组件的渲染阶段:

  • 搭建一个新 hooks 链表
  • 或者 复用 current.memoizedState 即旧fiber Hooks 链表

赋值给 currentlyRenderingFiber.memoizedStateworkInProgressHook

  1. 区分 Mount 和 Update 阶段,选择 Hooks 实现方式
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
  ? HooksDispatcherOnMount
  : HooksDispatcherOnUpdate;

根据 currentmemoizedState,判断函数组件状态:

  • 首次渲染Mount,ReactCurrentDispatcher.current = HooksDispatcherOnMount
  • 渲染更新Update,ReactCurrentDispatcher.current = HooksDispatcherOnUpdate

设置派发器 ReactCurrentDispatcher,确定如何执行组件中的 hooks。

  1. 执行组件函数(按顺序调用 Hooks)
let children = Component(props, secondArg);

Component(props, secondArg); 执行函数组件,函数中的 hooks 将被依次执行(useStateuseEffect ...)。

  1. 重置全局变量,准备下一轮渲染
ReactCurrentDispatcher.current = ContextOnlyDispatcher; 

renderLanes = NoLanes; 
currentlyRenderingFiber = (null: any); 

currentHook = null; 
workInProgressHook = null;

ReactCurrentDispatcher.current = ContextOnlyDispatcher; 设置默认的派发器,避免 hooks 在非React函数中被调用。

最后,将相关变量重新置空。

小结

renderWithHooks 作为执行函数组件的核心,主要作用为:

  1. 设置当前正在渲染的 Fiber 节点
  2. 重置 workInProgress fiber 状态
  3. 区分 Mount 和 Update 阶段,选择 Hooks 实现方式
  4. 执行组件函数(按顺序调用 Hooks)
  5. 重置全局变量,准备下一轮渲染

接下来,我们继续探索 Hooks 的实现方式。

ReactCurrentDispatcher

上文说到 renderWithHooks 函数,根据函数组件的渲染状态,设置不同的派发器。

// 函数组件为 首次渲染Mount 阶段
const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  useCallback: mountCallback,
  useContext: readContext,
  useEffect: mountEffect,
  useImperativeHandle: mountImperativeHandle,
  useLayoutEffect: mountLayoutEffect,
  useInsertionEffect: mountInsertionEffect,
  useMemo: mountMemo,
  useReducer: mountReducer,
  useRef: mountRef,
  useState: mountState,
  useDebugValue: mountDebugValue,
  useDeferredValue: mountDeferredValue,
  useTransition: mountTransition,
  useMutableSource: mountMutableSource,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,

  unstable_isNewReconciler: enableNewReconciler,
};
// 函数组件为 渲染更新Update 阶段
const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  useCallback: updateCallback,
  useContext: readContext,
  useEffect: updateEffect,
  useImperativeHandle: updateImperativeHandle,
  useInsertionEffect: updateInsertionEffect,
  useLayoutEffect: updateLayoutEffect,
  useMemo: updateMemo,
  useReducer: updateReducer,
  useRef: updateRef,
  useState: updateState,
  useDebugValue: updateDebugValue,
  useDeferredValue: updateDeferredValue,
  useTransition: updateTransition,
  useMutableSource: updateMutableSource,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,

  unstable_isNewReconciler: enableNewReconciler,
};

可以看到在不同阶段,Hooks 的实现方式(函数)不同。

例如 useState

  • Mount 阶段 —— useState: mountState
  • Update 阶段 —— useState: updateState

下文以 useState 为例,看看 hooks 在不同阶段是如何执行的。

useState

mountState

function mountState(initialState) {
  // 1. 挂载并获取最新的 Hooks 链表节点
  const hook = mountWorkInProgressHook();
  // 2. 对 hook 初始化赋值
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue = {
    pending: null,  // 待更新队列
    lanes: NoLanes, // 更新队列的优先级
    dispatch: null, // 负责更新state的方法
    lastRenderedReducer: basicStateReducer, // 最后一次渲染的reducer
    lastRenderedState: (initialState: any), // 最后一次渲染的state值
  };
  hook.queue = queue;
  // 3. 确定触发函数更新的方法
  const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue))
  return [hook.memoizedState, dispatch];
}
  1. 挂载并获取最新的 Hooks 链表节点
const hook = mountWorkInProgressHook();

处于 Mount阶段 的函数组件,每一次 Hooks 的执行,比如 useStateuseEffectuseRef 都会调用 mountWorkInProgressHook 函数,创建新 hook 节点并挂载,以获取最新的 Hooks 链表节点。

  1. hook 初始化赋值
if (typeof initialState === 'function') {
  initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
   pending: null,  // 待更新队列
   lanes: NoLanes, // 更新队列的优先级
   dispatch: null, // 负责更新state的方法
   lastRenderedReducer: basicStateReducer, // 最后一次渲染的reducer
   lastRenderedState: (initialState: any), // 最后一次渲染的state值
};
hook.queue = queue;
  • useState(initialState)

如传递函数作为参数,initialState 被视为初始化函数,调用取值。

  • hook.memoizedState = hook.baseState = initialState;

initialState 赋值给 hook 节点的 memoizedState当前状态 和 baseState基础状态,作为初始状态。

  • hook.queue = queue;

新建空的队列,作为 hook 的更新队列。

  1. 确定触发函数更新的方法
  • queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)

dispatchSetState.bind 绑定了 目标fibercurrentlyRenderingFiber 和 更新队列queue,但没有指定 action

通过调用 setState,触发函数组件的状态更新 dispatchSetState

  • return [hook.memoizedState, dispatch];

返回 初始状态hook.memoizedState 和 更新状态的函数dispatchsetXXX(nextState)

mountWorkInProgressHook

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,
  };

  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

mountWorkInProgressHook 函数

  • 每次调用新建一个 hook 节点,const hook: Hook = { //... }
  • 首次执行 Hooks,即 当前正在构建Fiber的Hooks链表 为空,workInProgressHook === null
    • workInProgressHook = hook;
      • hook 作为链表头节点
    • currentlyRenderingFiber.memoizedState = workInProgressHook
      • workInProgressHook 作为 workInProgress 的链表
      • (前文说过 currentlyRenderingFiber 指向最新的Fiber,fiber.memoizedState 属性用于存储 Hooks 链表的头节点)
  • 非首次执行 Hooks
    • workInProgressHook.next = hook;
      • hook 挂载到链表末尾
    • workInProgressHook = workInProgressHook.next
      • workInProgressHook 指向最新的 hook
  • 返回 新Fiber 的 Hooks 链表,return workInProgressHook;

updateState

function updateState(initialState) {
  return updateReducer(basicStateReducer, (initialState: any));
}

function basicStateReducer(state, action) {
  return typeof action === 'function' ? action(state) : action;
}

useState 在 Update阶段,本质是调用提前设置好 reducerupdateReducer 函数(reducer = basicStateReducer)。

updateReducer

抛开_DEV_相关的调试代码和一些注释,精简 updateReducer 函数:

function updateReducer(reducer, initialArg, init) {
  // 1. 拷贝 current hook 到 workInProcess hook,并返回最新的 Hooks 链表节点
  const hook = updateWorkInProgressHook();

  // 2. 读取 hook.queue 中待执行的更新队列 pending,合并到 current.baseQueue 队尾
  const queue = hook.queue;
  queue.lastRenderedReducer = reducer;
  const pendingQueue = queue.pending;
  
  const current = currentHook;
  let baseQueue = current.baseQueue;
  
  if (pendingQueue !== null) {
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      // 构成环装链表
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  // 3. 遍历 baseQueue 待更新队列,计算出最新的 state
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;

    do {
      // 
      // const updateLane = removeLanes(update.lane, OffscreenLane);
      // const shouldSkipUpdate = ...
      // if (shouldSkipUpdate) {
      //   ...
      //   标记被跳过的更新
      //   markSkippedUpdateLanes(updateLane);
      // }
      
      // 随着遍历 不断更新 newState
      if (update.hasEagerState) {
        newState = update.eagerState;
      } else {
        const action = update.action;
        newState = reducer(newState, action);  // 得到最新的 state 值
      }
      update = update.next;
    } while (update !== null && update !== first);
    
    // 4. 判断是否存在未完成的更新
    if (newBaseQueueLast === null) {
      // 仅在全部更新完成时,更新 `newBaseState`
      newBaseState = newState;
    } else {
      // 构建环形链表,供恢复时使用
      newBaseQueueLast.next = newBaseQueueFirst;
    }
    // 当 state 值未变化时,跳过渲染
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    // 5. 更新 workInProgress Hook 中对应的值
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  const dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch];
}

Update阶段,useState/useReducer 要做的事就很多了,但我们可以简单理解为:

  1. currentHook 中对应的 hook 节点,克隆到 workInProgressHook 中,即 复用旧 Fiber 的 Hooks 链表
  2. 读取更新队列 queue合并执行所有更新(dispatchAction
  3. 计算出最新状态 newState(期间可能跳过、中断、恢复),更新 hook 的状态

看看代码中的具体实现:

  1. 拷贝 current hookworkInProcess hook,并返回最新的 Hooks 链表节点
const hook = updateWorkInProgressHook();

处于 Update阶段 的函数组件,每一次 Hooks 的执行,比如 useStateuseEffectuseRef 都会调用 updateWorkInProgressHook 函数,将 currentHook 链表 clone给 WorkInProgressHook 链表,并获取最新的 Hooks 链表节点。

hook(current -> workInProcess)

  1. 读取 hook.queue 中待执行的更新队列 pending,合并到 current.baseQueue 队尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const pendingQueue = queue.pending;

const current = currentHook;
let baseQueue = current.baseQueue;

if (pendingQueue !== null) {
  if (baseQueue !== null) {
    const baseFirst = baseQueue.next;
    const pendingFirst = pendingQueue.next;
    // 构成环装链表
    baseQueue.next = pendingFirst;
    pendingQueue.next = baseFirst;
  }
  current.baseQueue = baseQueue = pendingQueue;
  queue.pending = null;
}

pendingQueue = queue.pending;,读取了待执行的更新队列

pendingFirst = pendingQueue.next;,提供了 pendingQueue 的头节点

baseQueue.next = pendingFirst;,将 pendingQueue 合并到 baseQueue

在 React 中,渲染可能会被高优先级任务中断(如用户交互),此时 baseQueue 还未全部执行,所以使用合并链表的方式,而不是直接覆盖队列。

  1. 遍历 baseQueue 待更新队列,计算出最新的 state
if (baseQueue !== null) {
  const first = baseQueue.next;
  let newState = current.baseState;
  let newBaseState = null;
  let newBaseQueueFirst = null;
  let newBaseQueueLast = null;
  let update = first;

  do {
    // 
    // const updateLane = removeLanes(update.lane, OffscreenLane);
    // const shouldSkipUpdate = ...
    // if (shouldSkipUpdate) {
    //   ...
    //   标记被跳过的更新
    //   markSkippedUpdateLanes(updateLane);
    // }
    
    // 随着遍历 不断更新 newState
    if (update.hasEagerState) {
      newState = update.eagerState;
    } else {
      const action = update.action;
      newState = reducer(newState, action);  // 得到最新的 state 值
    }
    update = update.next;
  } while (update !== null && update !== first);
  • do { ... } while ( ... )

例如代码中多次调用 setCount

setCount(count=> count + 1);
setCount(count=> count + 1);
setCount(count=> count + 1);

每次 setCount 产生的 Update 都会被放入 pendingQueue 从而被合并到 baseQueue,从而计算出最终的状态 newState

  • newState = reducer(newState, action);

调用reducer -> action,根据上一次 update 产生的 newState,并更新状态 newState =

update.action 就是 setState(nextState) 中的 nextState,为一个新值或更新函数。

function basicStateReducer(state, action) { 
    return typeof action === 'function' ? action(state) : action; 
}

调用流程为 dispatch(fiber, queue)(nextState) -> dispatchAction(fiber, queue, action)

  • if (update.hasEagerState)

dispatchAction(dispatchSetState) 中,会对比新旧状态 is(eagerState, currentState) 是否相同,从而决定是否执行更新

  1. 判断是否存在未完成的更新
if (newBaseQueueLast === null) {
  // 仅在全部更新完成时,更新 `newBaseState`
  newBaseState = newState;
} else {
  // 构建环形链表,供恢复时使用
  newBaseQueueLast.next = newBaseQueueFirst;
}

上文说过,渲染可能会被高优先级任务中断(如用户交互)。

newBaseQueueLast !== null,即更新队列还有任务时,只会更新 当前状态hook.memoizedState,不更新 基础状态hook.baseState

  1. 更新 workInProgress Hook 中对应的值
// 当 state 值未变化时,跳过渲染
if (!is(newState, hook.memoizedState)) {
  markWorkInProgressReceivedUpdate();
}

hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;

queue.lastRenderedState = newState;

最后更新 workInProgressHook 对应的状态。

queue.lastRenderedState = newState; 记录最后的状态值,方便下次更新时对比。

updateWorkInProgressHook

function updateWorkInProgressHook(): Hook {
  // 移动 currentHook 指针
  let nextCurrentHook: null | Hook;
  if (currentHook === null) {
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    nextCurrentHook = currentHook.next;
  }
  // 移动 workInProgressHook 指针
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
    currentHook = nextCurrentHook;

    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };

    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

函数看似较长,但目的只有一个 —— 将旧节点的 hook 拷贝出来,组成 workInProgressHook 链表

  • 每次 clone 一个 current hook 节点,const newHook: Hook = { //... }
  • 首次执行 Hooks,即 当前正在构建Fiber的Hooks链表 为空,workInProgressHook === null
    • workInProgressHook = newHook;
      • newHook 作为链表头节点
    • currentlyRenderingFiber.memoizedState = workInProgressHook
      • workInProgressHook 作为 workInProgress 的链表
      • (前文说过 currentlyRenderingFiber 指向最新的Fiber,fiber.memoizedState 属性用于存储 Hooks 链表的头节点)
  • 非首次执行 Hooks
    • workInProgressHook.next = newHook;
      • newHook 挂载到链表末尾
    • workInProgressHook = workInProgressHook.next
      • workInProgressHook 指向最新的 hook
  • 返回 新Fiber 的 Hooks 链表,return workInProgressHook;

总结

简单回顾流程:

  1. 渲染入口
    • renderWithHooks 设置 currentlyRenderingFiber 和 ReactCurrentDispatcher
  2. Hooks 调用
    • useStateuseEffect 等从 ReactCurrentDispatcher 获取当前实现。
  3. 状态管理
    • Hooks 通过 currentHook 和 workInProgressHook 操作新旧 Fiber 的链表。
  4. 提交结果
    • 更新完成后,workInProgress Fiber 成为新的 current Fiber。

Hook

  1. 核心结构
    • 每个 Hook 以单向链表形式存储,next 指针连接下一个 Hook。
    • 严格依赖调用顺序,确保多次渲染时顺序一致。
    • fiber.memoizedState 属性保存了 当前fiber树的 Hooks链表的 头节点。
  2. 关键属性
    • memoizedState:当前状态(如 stateeffect 对象、ref 值)。
    • baseState:更新计算的基准状态(用于中断恢复)。
    • baseQueue:未完成的更新队列(持久化存储)。
    • queue:新触发的更新队列(临时环形链表)。
    • ...
  3. 工作机制
    • 挂载阶段
      • 初始化 Hook 链表,绑定 hook.memoizedState(如 useState(initialValue))。
      • 创建 queuedispatch 函数(如 setState
      • 返回初始值和触发更新的函数(如return [hook.memoizedState, dispatch]
    • 更新阶段
      • 合并更新队列,将新触发的 queue 合并到 baseQueue
      • 基于 baseState 遍历 baseQueue,执行 reducer 计算出最新的 state
      • 更新 workInProgressHook 链表中的属性值,memoizedStatebaseStatebaseQueue
    • 并发模式
      • 优先级调度,高优先级更新可中断低优先级任务,优先渲染。
      • 中断恢复,未完成的更新保留在 baseQueue,下次渲染继续处理。

最后,我们再看看以下规则:

1. 不要在条件、循环和嵌套函数中使用 Hook?

因为 hook 链表需要保证一致性,updateWorkInProgressHook 会clone 旧fiber的 hook节点,给新fiber 使用,错误的顺序、数量 会导致 识别错乱、链表断裂的问题。

2. 不要在普通的 JavaScript 函数中调用 Hook?

Hook 的执行依赖于当前组件的 Fiber节点。

并且 ReactCurrentDispatcher.current 会被默认设置为 ContextOnlyDispatcher,使用 Hooks 时会报错。

结语

希望本文能对你有所帮助。持续更新前端知识,脚踏实地不水文,真的不关注一下吗~

写作不易,如果有收获希望 点赞、收藏 🌹

才疏学浅,如有问题或建议还望指教~

参考

# 【前端开发】深度学习React Hooks【前端基础】

# 「react进阶」一文吃透react-hooks原理