Skip to content

useState

在说明useState之前,我们得了解一下 Hooks的作用是什么,useState的作用是什么?

Hooks的作用

Hooks的出现使得React的FunctionalComponent有了对标ClassComponent的可能,那么ClassComponent具有的私有变量、生命周期管理的能力 Hooks是如何实现的呐。

带这这个问题,我们就可以知道Hooks在聚合一个功能特性的时候,怎么去维护FunctionalComponent在初始化、更新、销毁三个状态的情况下使得函数也拥有了私有属性的能力且能在不同的生命周期实现不同的功能。

useState的作用是什么

js
const [state, setState] = useState(initialState);

返回一个 state,以及更新 state 的函数。

初始渲染期间,返回的状态 (state) 与传入的第一个参数 (initialState) 值相同。

后续的重新渲染中,useState 返回的第一个值将始终是更新后最新的 state。

从官网的说明中我们可以看到其区别于函数中的变量的地方在于 在函数组件初次执行的时候使用的是(initialState) 值,但是在重新渲染的时候使用的却是更新后的值。

那么useState(1) 在函数更新执行的时候 怎么知道state的值是 2... 而不是我们初始化的值 1,useState如何保存函数执行的时候 state的状态值的。

其实这个跟classComponent一样,class对象为什么可以在缓存当前状态的值 就是因为Class对象在初次创建的时候会生成一个实例对象,其私有属性都缓存在这个实例对象上,那么class.render等方法执行的时候就可以通过方法实例对象的属性去获取当前状态下的值了。

那么对于FunctionalComponent本身其是一个函数,其不会产生状态,但是其返回的值为FiberNode对象就可以作为一个实例对象去缓存当前状态下的值。这就是函数式组件为什么可以存在私有变量的原因。

栗子

js
const App = props => {
    // 添加一个 hook
    const [visible, setVisible] = React.useState(true)
    // 添加一个 hook
    const [num, setNum] = React.useState(1)

    const handleClickBtn = () => {
      // setVisible hook上添加一个 update
      setVisible(!visible)
    }
    React.useEffect(() => {
      // setNum hook上添加一个 update
      setNum(num + 1)
      // setNum hook上添加一个 update
      setNum(num + 1)
    }, [])
    return (
      <div>
        <button onClick={handleClickBtn}>切换 - {visible && "是"}</button>
      </div>
    )
}

在父组件遇到App子组件的时候 会创建一个 App的 FiberNode对象,然后判断FiberNode的tag类型的时候发现其为 IndeterminateComponent 类型,从而进入到 mountIndeterminateComponent的流程,其核心代码为 renderWithHooks 。下面我们先了解一下对于这个栗子中Hooks的内容是如何进行保存的。

  • 执行 const [visible, setVisible] = React.useState(true)

    这是一个hook,那么就创建一个 hook对象保存到 FiberNode(App).memoizedState 上

  • 执行 const [num, setNum] = React.useState(1)

    又是一个hook,那么再创建一个 hook对象,发现FiberNode(App).memoizedState已经有值,那么就保存到 FiberNode(App).memoizedState.next上

image

这样就形成了如上图所示的 hook的 单向链表结构

hooks的数据状态如何保存

上面例子中,在App组件初始渲染的时候,我们知道state的值为 useState(1)的初始化值 1,那么在effect执行后更新到2、3的时候,这个时候函数式组件又会从上到下执行,那么这时候为什么React知道num的值是3 而不是 初始化值 1呐?

这就涉及到useState的值是如何保存和读取的,如果在初次渲染的时候使用初始化值,在更新渲染的时候使用新的状态值。

下面我们看一下 renderWithHooks中的部分代码

js

export function renderWithHooks<Props, SecondArg>(...): any {

  workInProgress.memoizedState = null;
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  // 这就是 React提供的Hooks方法的执行函数
  // 其在组件初始化渲染的时候 使用 HooksDispatcherOnMount里面的方法 
  // 在   组件更新渲染的时候 使用 HooksDispatcherOnUpdate里面的方法
  // 如 useState 在 初始化的时候使用 mountState 在更新渲染的时候使用 updateState
  
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  // 执行函数组件方法
  let children = Component(props, secondArg);
  // 重置
  ReactCurrentDispatcher.current = ContextOnlyDispatcher;
  
  currentHook = null;
  workInProgressHook = null;

  return children;
}

ReactCurrentDispatcher.current = current === null || current.memoizedState === null ? HooksDispatcherOnMount : HooksDispatcherOnUpdate;

通过 current === null || current.memoizedState === null 去区分函数式组件当前渲染是初次渲染还是更新渲染

然后根据不同的组件状态去通过不同的Hooks函数 执行方法

初始渲染 mountState

js
/**
 * useState 初次渲染执行函数
 * @param {*} initialState 初始化值
 * @returns
 */
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 获取或者创建一个 hook 对象
  const hook = mountWorkInProgressHook();
  // 支持useState(() => 1) 内容为函数类型
  if (typeof initialState === 'function') {
    // $FlowFixMe: Flow doesn't like mixed types
    initialState = initialState();
  }
  // 将初始化的值赋给 memoizedState 和 baseState
  hook.memoizedState = hook.baseState = initialState;
  // 生成 updateQueue
  const queue = (hook.queue = {
    pending: null,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),
  });
  // 生成 dispatch
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  // 返回  const [visible, setVisible] = React.useState(true)
  //
  return [hook.memoizedState, dispatch];
}

从源码上可以看出对于 useState的初次渲染的时候,其声明过程主要是在 workInProgress上构建hook对象,并将hook添加到workInProgress.memoizedState 对象上(如果已经存在hook,那么就添加到 hook.next上),从而形成一个hook的单项链表结构

然后通过dispatchAction.bind 提供setState的方法,并将参数修改为 dispatchAction(FiberNode , queue , action )

更新渲染 updateState

js
// 就是使用的 updateReducer
function updateState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  return updateReducer(basicStateReducer, (initialState: any));
}

function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 复制历史FiberNode的hook,并生成新的hook
  const hook = updateWorkInProgressHook();
 {
    // 计算出新的 newState
    hook.memoizedState = newState;
  }

  return [hook.memoizedState, dispatch];
}

从mountState 和 updateState这么大的区别可以看出,为什么一个 state 可以在函数式组件中保存自己的状态。其核心就是

  • 在初次渲染的时候, 使用 initialState 的值作为 state ,并将当前 hook 缓存到 FiberNode.memoizedState上
  • 在更新渲染的时候,进行updateState,这时候走不同的逻辑,通过updateWorkInProgressHook() 获取FiberNode.memoizedState上对应的hook,然后根据Update Queue计算出最后的结果,并缓存到 hook.memoizedState 上

上面我们知道了在不同的生命周期获取正确的state,那么其怎么去保存这些数据的?

这就涉及到 Hook的结构的缓存,

hooks的保存结构

在初次渲染的时候,有一个关键的函数 mountWorkInProgressHook() ,这就是React的hook在组件初次渲染的时候,如果通过workInProgressHook 的全局变量去按照组件函数执行的顺序生成对应的 hook单向链表结构。

在组件更新渲染的情况下, 其调用的函数变成了updateWorkInProgressHook(),这个函数不是如何去创建hook并生成对应的 hook单向链表结构,而是根据相同的顺序去获取 mount上面的此下标下的hook进行复用,并构建新的 memoizedState

mountWorkInProgressHook()

hook对象的创建

js
/**
 * beginWork 初次渲染的时候 有一个Hooks就生成一个 hook对象,然后按照链表的结构保存到 workInProgreee.memoizedState上
 *  然后再将workInProgressHook指向当前的 hook对象
 * @returns
 */
function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    // 用于保存一次更新渲染流程的初始值 --- 对应的是 render的值
    memoizedState: null,
    // 用于保存一次渲染流程中多个update的值
    baseState: null,

    baseQueue: null,
    // 保存了 update 更新链
    queue: null,
    // 指向下一个hook
    next: null,
  };

  // 如果当前函数组件第一次初始化 Hooks
  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;
}

updateWorkInProgressHook()

js
/**
 * 函数式组件
 *  组件更新渲染的时候,执行到 useState等Hook 按照新FiberNode.alternate(久的FiberNode)的链表去在FiberNode.memoizedState上复制hook链;
 * @returns
 */
function updateWorkInProgressHook(): Hook {
  
  let nextCurrentHook: null | Hook;
  // currentHook为上一个Hook
  // 1. 如果 currentHook === null 说明这是处理的第一个 hook 那就通过 FiberNode.alternate.memoizedState 获取旧的FiberNode的第一个hook
  // 2. 如果 currentHook !== null 说明这是第二个及后面的 这时候就通过 currentHook nextCurrentHook 获取到当前hook在初次渲染的时候创建的 hook对象内容
  if (currentHook === null) {
    // 获取FiberNode 的 原来的FiberNode
    const current = currentlyRenderingFiber.alternate;
    // 有久的FiberNode
    if (current !== null) {
      // 那就将 nextCurrentHook 指向第一个 hook
      nextCurrentHook = current.memoizedState;
    } else {
      nextCurrentHook = null;
    }
  } else {
    // 将上一个 Hook.next 作为当前的 hook
    nextCurrentHook = currentHook.next;
  }

  // 跟 currentHook、nextCurrentHook的逻辑一样
  // 通过 workInProgressHook 、nextWorkInProgressHook
  // 在新的FiberNode.memoizedState上按照 nextCurrentHook的数据生成新的hook单向链表结构
  // 注意:
  // 1. nextWorkInProgressHook 为 null
  // 生成新的hook(newHook)的时候,其newHook.next === null
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }

  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it.
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;

    currentHook = nextCurrentHook;
  } else {
    // Clone from the current hook.
    // 将next作为当前处理的 hook
    currentHook = nextCurrentHook;
    // 生成一个新的 hook
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,

      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,

      next: null,
    };
    // 将新的hook单向链表添加到新的FiberNode.memoizedState上
    if (workInProgressHook === null) {
      // This is the first hook in the list.
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // Append to the end of the list.
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  return workInProgressHook;
}

image

hook结构

  • memoizedState

    用于保存一次更新渲染流程的初始值 --- 对应的是 render 的值

  • baseState

    用于保存因为优先级不同导致Update跳过之前计算的最终state的值(newState),如下述栗子的 "A",

    从而为下次计算不会因为之前的update不再执行,从而导致state的值丢失 (ABCD不会变成 BCD)

  • baseQueue

    保存因为优先级不同导致Update不执行的第一个跳过的链表,在下次执行的时候作为baseQueue的头部

  • queue

    保存了此次触发中产生的Update,用于在组件render的时候根据update的链表计算出最终的值

  • next

    指向下一个hook, 从而形成一个 hook的单向链表结构

触发修改

上文都是说的如何在初次渲染或者更新渲染的时候 如何创建 hook单向链表、复用hook单项链表和获取state

下面我们主要看其第二个值 触发修改

dispatchAction

需要注意的是,dispatchAction 不是我们想象的怎么去计算出新的 state,其只是根据你的dispatch去创建一个Update对象,然后在组件更新渲染的时候,根据update queue去计算出真正的值

其主要逻辑如下:

  1. 生成Update

  2. 将Update加入到hook.pending 环状单向链表中去

  3. 是否触发更新(scheduleUpdateOnFiber(fiber, lane, eventTime))

    注意: 这里是触发更新,不是更新组件

    • 如果在当前函数组件function执行的的过程中触发更新,那就直接跳过触发更新
    • 如果是最高优先级的更新且前后值相同 那么也跳过触发更新

最后如果我们一个事件中进行了多次的 dispatch,那么这个过程就是将这些dispath按照执行的顺序,收集到 hook.pending中去。

最后的结果就是:

js
React.useEffect(() => {
      // setNum hook上添加一个 update
      setNum(num => num + 1)
      // setNum hook上添加一个 update
      setNum(num => num + 2)
      // setNum hook上添加一个 update
      setNum(num => num + 3)
      // setNum hook上添加一个 update
      setNum(num => num + 4)
}, [])

image

注意

  1. pending本身不是我们想象的第一个dispatch(num1) 而是最后一个dispatch(num4)
  2. 第一个dispatch : pending.next
js
/**
 * 满满的Redux风格
 * Hooks中 useState useReducer 等触发变量修改的方法
 * @param {*} fiber action的FiberNode对象
 * @param {*} queue hooks的queue对象
 * @param {*} action
 * @returns
 */
function dispatchAction<S, A>(
  fiber: Fiber,
  queue: UpdateQueue<S, A>,
  action: A,
) {
  if (__DEV__) {
    if (typeof arguments[3] === 'function') {
      console.error(
        "State updates from the useState() and useReducer() Hooks don't support the " +
          'second callback argument. To execute a side effect after ' +
          'rendering, declare it in the component body with useEffect().',
      );
    }
  }
  // 获取事件的时间
  const eventTime = requestEventTime();
  // 获取优先级
  const lane = requestUpdateLane(fiber);
  // 生成Update对象 ,下面就是构建这个对象
  const update: Update<S, A> = {
    lane,
    // 计算的方法或者值  setNum((num) => num +1) -> num => num + 1
    action,
    eagerReducer: null,
    eagerState: null,
    next: (null: any),
  };

  // Append the update to the end of the list.
  // 将Update添加到hook.pending中,并通过next形成一个单项圆环链表结构
  const pending = queue.pending;
  if (pending === null) {
    // This is the first update. Create a circular list.
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
  // 获取原来的FiberNode
  const alternate = fiber.alternate;
  if (
    // 触发更新的fiber === 当前渲染的 fiber
    // 如 我们在渲染函数中直接调用 setState()  function App(){  const [num, setNum] = React.useState(1) ; setNum(2); return xxx  }
    // 那么就不添加了
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  ) {
    // This is a render phase update. Stash it in a lazily-created map of
    // queue -> linked list of updates. After this render pass, we'll restart
    // and apply the stashed updates on top of the work-in-progress hook.
    didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  } else {
    // 如果是一个最高优先级的更新
    // 那么直接判断是否需要更新
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // The queue is currently empty, which means we can eagerly compute the
      // next state before entering the render phase. If the new state is the
      // same as the current state, we may be able to bail out entirely.
      // 上次的执行dispatch
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
        if (__DEV__) {
          prevDispatcher = ReactCurrentDispatcher.current;
          ReactCurrentDispatcher.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
        }
        try {
          const currentState: S = (queue.lastRenderedState: any);
          // 计算出新的state
          const eagerState = lastRenderedReducer(currentState, action);
          // Stash the eagerly computed state, and the reducer used to compute
          // it, on the update object. If the reducer hasn't changed by the
          // time we enter the render phase, then the eager state can be used
          // without calling the reducer again.
          update.eagerReducer = lastRenderedReducer;
          update.eagerState = eagerState;
          // 如果值没有改变,那就不触发更新
          if (is(eagerState, currentState)) {
            // Fast path. We can bail out without scheduling React to re-render.
            // It's still possible that we'll need to rebase this update later,
            // if the component re-renders for a different reason and by that
            // time the reducer has changed.
            return;
          }
        } catch (error) {
          // Suppress the error. It will throw again in the render phase.
        } finally {
          if (__DEV__) {
            ReactCurrentDispatcher.current = prevDispatcher;
          }
        }
      }
    }
    if (__DEV__) {
      // $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
      if ('undefined' !== typeof jest) {
        warnIfNotScopedWithMatchingAct(fiber);
        warnIfNotCurrentlyActingUpdatesInDev(fiber);
      }
    }
    scheduleUpdateOnFiber(fiber, lane, eventTime);
  }

  if (__DEV__) {
    if (enableDebugTracing) {
      if (fiber.mode & DebugTracingMode) {
        const name = getComponentName(fiber.type) || 'Unknown';
        logStateUpdateScheduled(name, lane, action);
      }
    }
  }

  if (enableSchedulingProfiler) {
    markStateUpdateScheduled(fiber, lane);
  }
}

State的真正计算过程

对于一个hook的一次流程中其真正计算state的过程不是在dispatch的过程中,而是在 useState的过程中。重新回到上面 mountState和updateState的代码

初次渲染

初次渲染的计算很简单,就是返回intivitalValue的值, 这也是我们为什么说 useState(func) 这个func只执行一次

更新渲染

在上面我们通过dispatch生成了好多Update并保存在 hook.pending上,那么在updateState的时候,就会根据这些Update去计算出最终的state,并赋给 hook.memoizedState 。

下面我们按照几个情况去分别讲解计算过程。

有四个dispatch,分别为 A B C D,其优先级分用1 2标识,结果就按照 字符串加的过程进行计算

  1. A1 -> B1 -> C1 -> D1

这是比较正常的更新流程,其优先级都为1,其输出结果为 ABCD

在执行updateReducer的时候,其baseQueue === null , pending === A1-> D1,那么会将current.baseQueue = pendingQueue

然后按照都在 1的优先级下 执行 newState = reducer(newState, action);

  • A1的时候

    • newState : "" -> "A"
    • newBaseState : null;
    • newBaseQueueFirst = null ;
    • newBaseQueueLast = null
  • B1的时候

    • newState : "A" -> "AB"
    • newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null
  • C1的时候

    • newState : "AB" -> "ABC"
    • newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null
  • D1的时候

    • newState : "ABC" -> "ABCD"
    • newBaseState : null; newBaseQueueFirst = null ; newBaseQueueLast = null

最后

hook.memoizedState === "ABCD";

hook.baseState === "ABCD"

hook.baseQueue === null

  1. A1 -> B2 -> C1 -> D2

如果出现不同优先级的Update,那么因为updateReduer的时候会根据isSubsetOfLanes(renderLanes, updateLane) 判断只执行当前优先级下的Update,从而导致 B2 D2不会执行。但是 D2的值又依赖于前面C1的值,为了保持 1. 优先级不同不处理 2. 保持state的值计算的连续性,从而在hook上多了两个属性 baseState 和 baseQueue

  • baseState

    保存了当前hook中update链表中第一个被跳过的update之前newState的值 ,在此例子就是 "A"

    在下一次优先级执行中可以根据这个值恢复被跳过的update之后链表的初始值(下次更新就是从B2开始了)

  • baseQueue

    保存了上次优先级中因优先级不同从而跳过执行的第一个update及之后的链表,在此例子就是 "B2 -> C1 -> D2" , C1会再次被执行

下面我们看源码并讲解

  • A1

    • 执行A1的时候, baseQueue == null ; pendingQueue === A1 -> B2 -> C1 -> D2 , 因为pendingQueue不为null 从而baseQueue === pendingQueue
    • 执行do中else的过程,并计算newState,从而生成下面结果
    js
    newState           :   "" -> "A"
    newBaseState       = null;
    newBaseQueueFirst  = null;
    newBaseQueueLast   = null;
  • B2

    • 执行B2的时候,发现!isSubsetOfLanes(renderLanes, updateLane) === true;这时候生成B2Update的副本,且因为newBaseQueueLast === null ,从而使得
    js
    newState           =   "" -> "A"
    newBaseState       = newState = "A" ;
    newBaseQueueFirst  = B2;
    newBaseQueueLast   = B2;
  • C1

    • 执行C1的时候,发现 !isSubsetOfLanes(renderLanes, updateLane) === false;,又进入计算newState的过程,但是这时候与A1不同的是 newBaseQueueLast ! == null ; 从而使得 newBaseQueueLast 、newBaseQueueFirst 都进行了修改
    js
    newState           =   "A" -> "AC"
    newBaseState       = newState = "A" ; // 没有进行赋值
    newBaseQueueFirst  = B2;
    newBaseQueueLast   = C1 -> B2;
  • D2

    • 执行D2的时候,发现!isSubsetOfLanes(renderLanes, updateLane) === true;这时候生成D2Update的副本,且因为newBaseQueueLast === null ,从而使得
    js
    newState           =   "AC" -> "AC"
    newBaseState       = newState = "A" ;
    newBaseQueueFirst  = B2;
    newBaseQueueLast   = D2 -> C1 -> B2;  // 从而形成了一个从第一个跳过执行的udpate的update环状单向链表

在2优先级任务执行的时候

这时候 current.baseQueue不为null ,current.baseState也不为null,那么就将形成一个 baseQueue -> pending的新的链表(B2 -> C1 -> D2 -> 新的update),其初始值为 "A"。

这样就保持着 A -> B -> C -> D的执行顺序,不会因为优先级的不同,导致setState的计算顺序产生错误

重点

  1. hook和update的结构

  2. mount和update各自做了什么

  3. 如果保持state的计算连续性。

    始终按照setState执行的顺序去进行state的计算,不会因为update的优先级不同导致state的执行顺序产生错误

  4. 如何保证update变量不会因为优先级问题导致之前newState的丢失

    这就涉及到hook中两个保存state值的属性 memoizedState 和 baseState

    • memoizedState 保存了一次updateState的初始值 和 最终newState的值(最终newState的值可能因为优先级问题导致产生错误)
    • baseState 保存了第一次因优先级跳过的update之前newState的值,从而也为下次计算(从跳过开始)的初始值保证了基础