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

255 阅读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 时会报错。

结语

有同学可能发现了,没有讲 dispatchAction 相关的函数,我准备留到下一篇 《Hooks的应用与实现》 一起,感兴趣的话欢迎关注。

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

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

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

参考

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

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