一文解析UseState的的执行流程

前言

useState在React中无时无刻不在使用的一个hooks,有时候一些奇怪的问题和现象都会让使用者摸不着头脑,因此,深入理解源码,才能明白和避免问题的产生。

useState引入后会发生什么

tsx 复制代码
import { Button } from "antd";
import { useState } from "react";
const Index = () => {
  const [count, setCount] = useState(0);
  return (
    <>
        <div>数字:{count}</div>
        <Button onClick={() => setCount((v) => v + 1)}>点击加1</Button>
    </>
  );
};
export default Index;

这是一个简单的示例,当我们引入了useState,并存储了count变量,通过setCount来控制count的值,count是这个函数式组件的状态,setCount就是触发数据更新的函数。

接下来,我们看看源码,调用useState后,会执行那些逻辑。

文件位置:packages/react/src/ReactHooks.js

ts 复制代码
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
    // 重点在这个resolveDispatcher
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

可以看出 useState 的执行就等价于 resolveDispatcher().useState(initialState)

文件位置:packages/react/src/ReactCurrentDispatcher.js

ts 复制代码
const ReactCurrentDispatcher = {
  current: (null: null | Dispatcher),
};

通过类型可以看到 ReactCurrentDispatcher 不是 null,就是 Dispatcher,而在初始化时 ReactCurrentDispatcher.current 的值必为 null,因为此时还未进行操作

那么此时就很奇怪了,我们并没有发现 useState 是如何进行存储、更新的,ReactCurrentDispatcher.current 又是何时为 Dispatcher 的?

既然我们在 useState 自身中无法看到存储的变量,那么就只能从函数执行开始,一步一步探索 useState 是如何保存数据的。

函数式组件执行流程

JSX代码最终会走到beginWork这个方法中,而beginWork会走到mountIndeterminateComponent中,这个方法中会有一个函数叫renderWithHooks

renderWithHooks 就是所有函数式组件触发函数

文件位置:packages/react-reconciler/src/ReactFiberHooks

tsx 复制代码
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null,
  workInProgress: Fiber,
  Component: (p: Props, arg: SecondArg) => any,
  props: Props,
  secondArg: SecondArg,
  nextRenderLanes: Lanes,
): any {
  currentlyRenderingFiber = workInProgress;

  // memoizedState: 用于存放hooks的信息,如果是类组件,则存放state信息
  workInProgress.memoizedState = null;
  //updateQueue:更新队列,用于存放effect list,也就是useEffect产生副作用形成的链表
  workInProgress.updateQueue = null;

  // 用于判断走初始化流程还是更新流程
  ReactCurrentDispatcher.current =
    current === null || current.memoizedState === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate;

  // 执行真正的函数式组件,所有的hooks依次执行
  let children = Component(props, secondArg);

  finishRenderingHooks(current, workInProgress);

  return children;
}

function finishRenderingHooks(current: Fiber | null, workInProgress: Fiber) {
    
  // 防止hooks乱用,所报错的方案
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;

  const didRenderTooFewHooks =
    currentHook !== null && currentHook.next !== null;

  // current树
  currentHook = null;
  workInProgressHook = null;

  didScheduleRenderPhaseUpdate = false;
}

const HooksDispatcherOnMount: Dispatcher = {
  readContext,

  use,
  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,
  useSyncExternalStore: mountSyncExternalStore,
  useId: mountId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: mountActionState,
  useActionState: mountActionState,
  useOptimistic: mountOptimistic,
  useMemoCache,
  useCacheRefresh: mountRefresh,
};

const HooksDispatcherOnUpdate: Dispatcher = {
  readContext,

  use,
  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,
  useSyncExternalStore: updateSyncExternalStore,
  useId: updateId,
  useHostTransitionStatus: useHostTransitionStatus,
  useFormState: updateActionState,
  useActionState: updateActionState,
  useOptimistic: updateOptimistic,
  useMemoCache,
  useCacheRefresh: updateRefresh,
};

上述代码块忽略了DEV代码,我们先分析下 renderWithHooks 函数的入参。

  • current: 即 current fiber,渲染完成时所生成的 current 树,之后在 commit 阶段替换为真正的 DOM 树
  • workInProgress: 即 workInProgress fiber,当更新时,复制 current fiber,从这棵树进行更新,更新完毕后,再赋值给 current 树
  • Component: 函数组件本身;
  • props: 函数组件自身的 props;
  • secondArg: 上下文;
  • nextRenderLanes: 渲染的优先级。

可以看出初始化阶段和更新阶段调用的逻辑是不一样的,每个hooks在每个阶段都有对应的逻辑函数。

小知识:Fiber架构的三个阶段分别为reconcile、schedule、commit 阶段。

  • reconcile 阶段: vdom 转化为 fiber 的过程。
  • schedule 阶段:在 fiber 中遍历的过程中,可以打断,也能再恢复的过程。
  • commit 阶段:fiber 更新到真实 DOM 的过程。

renderWithHooks的执行过程

  1. 在每次函数组件执行之前,先将 workInProgress 的 memoizedState 和 updateQueue 属性进行清空,之后将新的 Hooks 信息挂载到这两个属性上,之后在 commit 阶段替换 current 树,也就是说 current 树保存 Hooks 信息;
  2. 然后通过判断 current 树是否存在来判断走初始化( HooksDispatcherOnMount )流程还是更新( HooksDispatcherOnUpdate )流程。而 ReactCurrentDispatcher.current 实际上包含所有的 Hooks,简单地讲,React 根据 current 的不同来判断对应的 Hooks,从而监控 Hooks 的调用情况;
  3. 接下来调用的 Component(props, secondArg) 就是真正的函数组件,然后依次执行里面的 Hooks;
  4. 最后提供整个的异常处理,防止不必要的报错,再将一些属性置空,如:currentHook、workInProgressHook 等。

# HooksDispatcherOnMount(初始化阶段)

在初始化阶段中,调用的是 HooksDispatcherOnMount,对应的 useState 所走的是 mountState

ts 复制代码
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const hook = (initialState);
  const queue = hook.queue;
  const dispatch: Dispatch<BasicStateAction<S>> = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any);
  queue.dispatch = dispatch;
  return [hook.memoizedState, dispatch];
}

function mountStateImpl<S>(initialState: (() => S) | S): Hook {
  const hook = mountWorkInProgressHook();
  // 如果初始值传入一个函数 这里执行函数拿到初始值
  if (typeof initialState === 'function') {
    const initialStateInitializer = initialState;
    // $FlowFixMe[incompatible-use]: Flow doesn't like mixed types
    initialState = initialStateInitializer();
  }
  hook.memoizedState = hook.baseState = initialState;
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  };
  hook.queue = queue;
  return hook;
}

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    baseQueue: null,
    queue: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}

function dispatchSetState<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
): void {
  const lane = requestUpdateLane(fiber);
  const didScheduleUpdate = dispatchSetStateInternal(
    fiber,
    queue,
    action,
    lane,
  );
  if (didScheduleUpdate) {
    startUpdateTimerByLane(lane, 'setState()', fiber);
  }
}

通过mountState可以看到,通过mountStateImpl方法里的mountWorkInProgressHook()方法拿到hooks,它的作用尤为重要,因为这个函数的作用是将 Hooks 与 Fiber 联系起来 ,并且你会发现,所有的 Hooks 都会走这个函数,只是不同的 Hooks 保存着不同的信息

mountWorkInProgressHook()方法会判断当前的Fiber是否有workInProgressHook,如果没有的,则创建,有的话,生成一个hook并赋值给workInprogressHook.next,并 workInProgressHook = workInProgressHook.next。最后返回workInprogressHook。

来看看 hook 值的参数:

  • memoizedState:用于保存数据,不同的 Hooks 保存的信息不同,比如 useState 保存 state 信息,useEffect 保存 effect 对象,useRef 保存 ref 对象;
  • baseState:当数据发生改变时,保存最新的值;
  • baseQueue:保存最新的更新队列;
  • queue:保存待更新的队列或更新的函数;
  • next:用于指向下一个 hook 对象。

那么 mountWorkInProgressHook 的作用就很明确了,每执行一个 Hooks 函数就会生成一个 hook 对象,然后将每个 hook 串联起来

特别注意:这里的 memoizedState 并不是 Fiber 链表上的 memoizedState,workInProgress 保存的是当前函数组件每个 Hooks 形成的链表。

再来看看dispatchSetState

dispatch 的机制就是 dispatchSetState,通过观察dispatchSetState发现,其实内部就是调用了dispatchSetStateInternal方法,我们来看看dispatchSetStateInternal方法

ts 复制代码
function dispatchSetStateInternal<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
  lane: Lane,
): boolean {
  const update: Update<S, A> = {
    lane,
    revertLane: NoLane,
    gesture: null,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };

  if (isRenderPhaseUpdate(fiber)) {
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher = null;
        try {
          const currentState: S = (queue.lastRenderedState: any);
          const eagerState = lastRenderedReducer(currentState, action);
          update.hasEagerState = true;
          update.eagerState = eagerState;
          if (is(eagerState, currentState)) {
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return false;
          }
        } catch (error) {
        } finally {
        }
      }
    }

    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      scheduleUpdateOnFiber(root, fiber, lane);
      entangleTransitionUpdate(root, queue, lane);
      return true;
    }
  }
  return false;
}

首先会生成一个update对象,用于记录更新信息,其次会 判断是否处于渲染阶段 :如果是渲染阶段,则将 update 放入等待更新的 pending 队列中,如果不是,就会获取最新的 state 值,从而进行更新。

值得注意的是:在更新过程中,也会判断很多,通过调用 lastRenderedReducer 获取最新的 state,然后进行比较(浅比较) ,如果相等则退出,这一点就是证明 useState 渲染相同值时,组件不更新的原因。

如果不相等,则会将 update 插入链表的尾部,返回对应的 root 节点,通过 scheduleUpdateOnFiber 实现对应的更新 ,可见 scheduleUpdateOnFiber 是 React 渲染更新的主要函数。

HooksDispatcherOnUpdate(更新阶段)

在更新阶段时,调用 HooksDispatcherOnUpdate,对应的 useState 所走的是 updateState

文件位置:packages/react-reconciler/src/ReactFiberHooks.js

ts 复制代码
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, initialState);
}

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

basicStateReducer 很简单,判断是否是函数,返回对应的值即可。

那么下面主要看 updateReducer 这个函数,在 updateReducer 函数中首先调用 updateWorkInProgressHook,我们先来看看这个函数,方便后续对 updateReducer 的理解。

updateWorkInProgressHookmountWorkInProgressHook 一样,当函数更新时,所有的 Hooks 都会执行

文件位置:packages/react-reconciler/src/ReactFiberHooks.js

ts 复制代码
function updateWorkInProgressHook(): Hook {
  // 初始化 `nextCurrentHook`(对应上一次渲染的 Hook)
  let nextCurrentHook: null | Hook;
   // 第一个hook渲染 currentHook为空 就说明是第一个hook
  if (currentHook === null) {
     // 对应的"旧" Fiber 节点(上次渲染的结果)
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
     // 拿到上一个旧Fiber数的第一个hook
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } 
  else {
    // 不是第一个hook 通过hooks链表拿到下一个hook
    nextCurrentHook = currentHook.next;
  }
 
  // 获取当前 WIP(Work In Progress)中的Hook
  let nextWorkInProgressHook: null | Hook;
  // workInProgressHook为null 就说明还在构建新的hook链表
  if (workInProgressHook === null) {
    //  `currentlyRenderingFiber`:当前正在渲染的 Fiber 节点(即 `WIP`, Work-In-Progress)
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    // 
    if (nextCurrentHook === null) {
      const currentFiber = currentlyRenderingFiber.alternate;
      if (currentFiber === null) {
        throw new Error(
          'Update hook called on initial render. This is likely a bug in React. Please file an issue.',
        );
      } 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;
}

updateWorkInProgressHook 执行流程:首先需要拿到旧Fiber树的Hook(nextCurrentHook、currentHook),这两个变量都是在操作旧Fiber树上的Hook,(workInProgressHook、nextWorkInProgressHook),这两个变量是操作当前Fiber树上的Hook,如果是第一次执行updateWorkInProgressHook方法,此时的currentHook即为空,会拿到上一个Fiber树的hook,如果workInProgressHook为null,就说明当前的workInProgressHook正在构建,nextWorkInProgressHook = currentlyRenderingFiber.memoizedState这块nextWorkInProgressHook会为null,所以会重新构建hook走到else的逻辑中。

掌握了 updateWorkInProgressHook执行流程后, 再来看 updateReducer 具体有哪些内容。

ts 复制代码
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {

  // 获取更新的hook,每个hook都会走
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;

  queue.lastRenderedReducer = reducer;

  const current: Hook = (currentHook: any);

  let baseQueue = current.baseQueue;
 
  // 在更新的过程中,存在新的更新,加入新的更新队列
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    // 如果在更新过程中有新的更新,则加入新的队列,有个合并的作用,合并到 baseQueue
    if (baseQueue !== null) {
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }

  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 isHiddenUpdate = updateLane !== update.lane;

      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        const clone: Update<S, A> = {
          lane: updateLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        
        // 合并优先级(低级任务)
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
         // 判断更新队列是否还有更新任务
        if (newBaseQueueLast !== null) {
          const clone: Update<S, A> = {
            lane: NoLane,
            action: update.action,
            hasEagerState: update.hasEagerState,
            eagerState: update.eagerState,
            next: (null: any),
          };
          
          // 将更新任务插到末尾
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }

        const action = update.action;
        
        // 判断更新的数据是否相等
        if (update.hasEagerState) {
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      // 判断是否还需要更新
      update = update.next;
    } while (update !== null && update !== first);

    // 如果 newBaseQueueLast 为null,则说明所有的update处理完成,对baseState进行更新
    if (newBaseQueueLast === null) {
      newBaseState = newState;
    } else {
      newBaseQueueLast.next = (newBaseQueueFirst: any);
    }

    // 如果新值与旧值不想等,则触发更新流程
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }

    // 将新值,保存在hook中
    hook.memoizedState = newState;
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }

  if (baseQueue === null) {
    queue.lanes = NoLanes;
  }

  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];
}

updateReducer 的作用是将待更新的队列 pendingQueue 合并到 baseQueue 上,之后进行循环更新,最后进行一次合成更新,也就是批量更新,统一更换节点。

这种行为解释了 useState 在更新的过程中为何传入相同的值,不进行更新,同时多次操作,只会执行最后一次更新的原因了。

相关推荐
呼啦啦嘎嘎3 小时前
rust中的生命周期
前端
岁月宁静3 小时前
前端添加防删除水印技术实现:从需求拆解到功能封装
前端·vue.js·人工智能
隐林3 小时前
如何使用 Tiny-editor 快速部署一个协同编辑器
前端
Mintopia3 小时前
🧠 对抗性训练如何增强 WebAI 模型的鲁棒性?
前端·javascript·人工智能
恋猫de小郭3 小时前
Flutter 在 iOS 26 模拟器跑不起来?其实很简单
android·前端·flutter
北城笑笑3 小时前
Git 10 ,使用 SSH 提升 Git 操作速度实践指南( Git 拉取推送响应慢 )
前端·git·ssh
FreeBuf_3 小时前
攻击者利用Discord Webhook通过npm、PyPI和Ruby软件包构建隐蔽C2通道
前端·npm·ruby
科技百宝箱3 小时前
02-如何使用Chrome工具排查内存泄露问题
前端·chrome
鹏多多4 小时前
React无限滚动插件react-infinite-scroll-component的配置+优化+避坑指南
前端·javascript·react.js