React-hook源码阅读 - useState

  • 关于React源码的阅读的版本都为18.2.0。 因为这一系列是源码系列, 故源码相关会讲得比较详细。
  • 如果对源码不太感冒的话可以直接看第三大标题的图示总结部分, 主流程也都呈现了

一、入口源码解析

为了让干扰最小化, 我们来个最简单的代码

js 复制代码
function App() {
  const [count, setCount] = useState(1);
  return (
    <div>
      <div onClick={() => setCount(count + 1)}>{count}</div>
    </div>
  );
}

关于整体的render过程我们之前在文章三 React源码阅读(三)- render已经接触过了, 其中我们在对Fiber进行构造的时候。 是通过在beginwork的阶段通过Swtich Case然后识别Fibertag再进入不同的流程中。

我们断点直接打到APP。可以看到在beginwork的时候判断其tagIndeterminateComponent(尚未确定具体类型的组件)。 然后调用了mountIndeterminateComponent。 里面又继续调用了renderWithHooks

renderWithHooks这个命名一看就是我们想要,需要了解的, 那我们直接聚焦到renderWithHooks

renderWithHooks

可以看到首先是对一些状态做了初始化。 然后给ReactCurrentDispatcher.current进行了赋值。如果是首次渲染的话赋值HooksDispatcherOnMount, 如果是更新的情况的话赋值HooksDispatcherOnUpdate。 接着调用了组件函数本身。 走入了我们的组件

js 复制代码
export function renderWithHooks<Props, SecondArg>(
  current: Fiber | null, // 当前页面使用的Fiber结构
  workInProgress: Fiber, // 当前正在构造的Fiber结构
  Component: (p: Props, arg: SecondArg) => any, // 当前处理的组件
  props: Props,  // 传递给组件的值
  secondArg: SecondArg,
  nextRenderLanes: Lanes,  // 下一次render的优先级
): any {
  renderLanes = nextRenderLanes;
  // 设置当前进行render的Fiber节点。 这个在后面会使用
  currentlyRenderingFiber = workInProgress;
  // 进行初始化
  workInProgress.memoizedState = null; 
  workInProgress.updateQueue = null;
  workInProgress.lanes = NoLanes;
  // 如果是初次渲染的时候除了hostRootFiber之外其他节点的current都为空
  // 故通过current判断是初次渲染还是更新
  // 然后挂载不同的Dispatcher
  ReactCurrentDispatcher.current =
      current === null || current.memoizedState === null
        ? HooksDispatcherOnMount
        : HooksDispatcherOnUpdate;
  // 这里去调用组件--------也就走入了我们的函数本身
  let children = Component(props, secondArg);
  ----------这里先打断, 后面的源码后续再看, 跟着代码逻辑先进入函数本身-------------------

HooksDispatcherOnMount/HooksDispatcherOnUpdate

上面我们也说到了, 这两玩意是赋值给ReactCurrentDispatcher.current。 两者都是对象。对象中的key都以hook的名称进行命名, value的话对于HooksDispatcherOnMount而言基本都是mountxxxx, 以mount开头。 对于HooksDispatcherOnUpdate而言就是基本都是updatexxxx, 以update开头。 两者分别对应的都是一些函数逻辑。 在后续讲解不同hook的时候会具体描述

这里如果你处于开发环境的话其实赋值的是HooksDispatcherOnMountWithHookTypesInDEVHooksDispatcherOnMountInDEV。 目的是在开发的时候React能够提供更多的警告和提示信息。 这部分我们不需要了解。 核心逻辑直接看线上版本即可

二、 useState源码解析

我们从useState的逻辑打开hook的大门。 主打一个举一反三。 故useState的相关逻辑会讲述的比较清晰。 在重要函数中使用的函数基本都会解析一番。 如果有疑问的直接点击目录进行定位即可

如图所示, 调用APP()之后进来了useStateuseState的代码很简单, 就调用了resolveDispatcher(), 该函数返回了dispatcher, 然后调用该dispatcheruseState方法, 那么我们先看看这个dispatcher是啥

js 复制代码
export function useState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  const dispatcher = resolveDispatcher();
  return dispatcher.useState(initialState);
}

resolveDispatcher

进来一看,其实就是赋值了ReactCurrentDispatcher.current。 诶, 这不就是我们上面讲述的东西吗。 此时为初次渲染。那么我们此时拿到的应该就是HooksDispatcherOnMount

  • 继续往下走。 可以看到调用dispatcher.useState(initialState);的时候。 那么实际上是调用了mountState

mountState

因为他的返回值就是useState的返回值。 故我们清楚最后返回的肯定是一个数组。 数组的首项为当前维护的state的值。 数组的第二项为可以修改state的函数。

故该函数的工作都是围绕此展开

函数组件不像类组件可以直接生成对应的实例。 故函数维护的状态都是挂载到Fiber节点上。 该函数的逻辑就是生成当前Fiber对应的hook对象, 对其进行各种初始化, 然后再挂载到当前FibermemoizedState。 最后返回的都是挂载当前hook上的东西

  • 最后返回的statehook.memoizedState
  • 最后返回的Set函数: hook.queue.dispatchdispatchSetState
js 复制代码
function mountState<S>(
  initialState: (() => S) | S,
): [S, Dispatch<BasicStateAction<S>>] {
  // 生成当前的hook对象, 并挂上链表
  const hook = mountWorkInProgressHook();
  // initalState是可以传入一个函数的。此时我们记录的值是该函数的返回值
  // 情景: 初始值需要通过复杂的计算获得
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  // 初始化当前hook的memoizedState和baseState, 都赋值为传入的initialState(或者调用得到的)
  hook.memoizedState = hook.baseState = initialState;
  // 生成一个queue对象
  const queue: UpdateQueue<S, BasicStateAction<S>> = {
    pending: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: (initialState: any),  // 这里也对其初始化了
  };
  // 也挂载到hook对象
  hook.queue = queue;
  // 这里其实就是将dispatchSetState赋值给了queue.dispatch和dispatch
  // 那么如果接下去如果触发state的改变的话就是触发了dispatchSetState
  const dispatch: Dispatch<
    BasicStateAction<S>,
  > = (queue.dispatch = (dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    queue,
  ): any));
  return [hook.memoizedState, dispatch];
}

mountWorkInProgressHook

这个函数还蛮重要经典的。 主要就是生成hook对象。 然后挂上当前Fiberhook链表(挂在memoizedState上)。 用workInProgressHook作为指针, 始终指向当前链表的最后一个。

js 复制代码
function mountWorkInProgressHook(): Hook {
   // 生成当前的hook对象
  const hook: Hook = {
    memoizedState: null,
    baseState: null,
    baseQueue: null,
    queue: null,
    next: null,  // 看到了next就应该知道他是要形成对应的链表
  };
  // 如果当前Fiber的workInProgressHook还没有初始化的时候对其进行初始化
  if (workInProgressHook === null) {
    // 前面我们在renderWithHooks的时候就设置了currentlyRenderingFiber为workInprogress, 也就是当前的Fiber
    // 故这里是当workInProgressHook指向当前生成的hook对象
    // 然后挂到当前Fiber的memoizedState上
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
  } else {
  // 如果已经初始化过的话, 那么就直接找到next挂上去。 
  // 且workInProgressHook指向他的next。
  // 让workInProgressHook始终指向链表的最后一个, 从而能够继续挂上其他hook对象
    workInProgressHook = workInProgressHook.next = hook;
  }
  // 返回当前指针
  return workInProgressHook;
}

既然我们提到了链表的思想。 那么我们不妨改一下代码。 我让代码中存在多个state, 然后我们再观察一下我们的链表。 看看是否符合预期

js 复制代码
 const [count, setCount] = useState(1);
 const [count1, setCount1] = useState(2);
 const [count2, setCount2] = useState(3);
 const [count3, setCount3] = useState(() => 4);

左边是我画的图, 右边是直接断点的截图。 爱看哪个看哪个hhh

dispatchSetState

继续看dispatchSetState。我们在前面的代码中使用click去调用setCount触发count的变化。 那么就自然会走到dispatchSetState函数中

js 复制代码
function dispatchSetState<S, A>(
  fiber: Fiber, // 这个之前挂载的时候就传入了, 为当前节点的Fiber
  queue: UpdateQueue<S, A>, // 当前节点的hook.queue
  action: A, // 这里的action就是我们传入的东西。 可以为值也可以是函数
) {
  // 获取优先级
  const lane = requestUpdateLane(fiber);
  // 生成了一个update对象
  const update: Update<S, A> = {
    lane,
    action,
    hasEagerState: false,
    eagerState: null,
    next: (null: any),
  };
  // 判断是否处于render更新阶段
  if (isRenderPhaseUpdate(fiber)) {
    // 是的话将update对象加入queue的pending链表上
    enqueueRenderPhaseUpdate(queue, update);
  } else {
    const alternate = fiber.alternate;
    // 判断是否已有更新
    // 这里可以定位到enqueueConcurrentHookUpdate看
    // 其中有对Fiber.lanes和alternate.lanes进行合并处理
    // 那么当我们再走进这个逻辑的时候,如果已经有过更新,此处就不会为NoLanes
    if (
      fiber.lanes === NoLanes &&
      (alternate === null || alternate.lanes === NoLanes)
    ) {
      // 在初始化queue对象的时候, 我们给queue.lastRenderedReducer就赋值了basicStateReducer
      const lastRenderedReducer = queue.lastRenderedReducer;
      if (lastRenderedReducer !== null) {
        let prevDispatcher;
         // 这里初始化的时候也是赋值了initialState
         const currentState: S = (queue.lastRenderedState: any);
         // 传入当前的state和传入的action, 拿到期望的更新之后的state
         const eagerState = lastRenderedReducer(currentState, action);
         // 表示该update对象的期望state已经计算出来了并赋值
         update.hasEagerState = true;
         update.eagerState = eagerState;
          // 进行浅比较, 如果一致的话将update推入栈之后直接return。 
          // 如果没有其他更新的话则不会进入调度过程了
          if (is(eagerState, currentState)) {
            // 推入栈
            enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
            return;
          }
      }
    }
    // 入栈入栈
    const root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);
    if (root !== null) {
      const eventTime = requestEventTime();
      // 开启调度
      scheduleUpdateOnFiber(root, fiber, lane, eventTime);
      entangleTransitionUpdate(root, queue, lane);
    }
  }
}

isRenderPhaseUpdate

这里是用来判断是否处于render阶段的。我们前面有涉及到currentlyRenderingFiber是在renderWithHooks的时候进行赋值的。 那renderWithHooks其实就是处于render阶段的。 在点击触发state的修改过程中。 我们触发onClick之后就直接走到了dispatchSetState,没有经过renderWithHooks, 故此时拿到的currentlyRenderingFiber就为空。 这里是判断了当前Fiber以及他的alternate Fiber。只要有一个处于render阶段就返回true。 否则返回false

js 复制代码
function isRenderPhaseUpdate(fiber: Fiber) {
  const alternate = fiber.alternate;
  return (
    fiber === currentlyRenderingFiber ||
    (alternate !== null && alternate === currentlyRenderingFiber)
  );
}

enqueueRenderPhaseUpdate

这里用于处理state触发变更时刚好处于render阶段的情况。 此时会将update对象存放到queuepending链上,也是以链表的方法存储。 在这个render过程之后, 我们将会重新启动更新

js 复制代码
**function enqueueRenderPhaseUpdate<S, A>(
  queue: UpdateQueue<S, A>,
  update: Update<S, A>,
) {
  didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
  const pending = queue.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  queue.pending = update;
}

最终形成的链表如图所示。 第一个update对象为queue.pending.next

basicStateReducer

对于action而言, 可以传入的参数有两种。 一种是直接传值。 一种是传入函数。 如果是函数的话函数的参数为当前的state。 故该函数就是处理了这两种情况。 这里的state就是目前最新的state, 我们常用函数的方式处理闭包可能带来的问题。

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

enqueueConcurrentHookUpdate

这里就是推入栈中保存。这里调用的是enququeUpdate。 我们之前在 React源码入门篇(一)--- enqueueUpdate 就已经讲过该逻辑的。 主要就是将queueupdate推入栈。 然后更新了Fiberlanes

js 复制代码
export function enqueueConcurrentHookUpdate<S, A>(
  fiber: Fiber,
  queue: HookQueue<S, A>,
  update: HookUpdate<S, A>,
  lane: Lane,
): FiberRoot | null {
  const concurrentQueue: ConcurrentQueue = (queue: any);
  const concurrentUpdate: ConcurrentUpdate = (update: any);
  enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
  return getRootForUpdatedFiber(fiber);
}

enqueueConcurrentHookUpdateAndEagerlyBailout

这里和enqueueConcurrentHookUpdate其实内容上基本一致。 都会把update推入栈中保存。 但是不一样的是, 这里不需要return东西出去了。 以及他的上下文中。 执行完这个函数之后直接return了。

既然return了如果没有其他State变更的话就不会走到下面的scheduleUpdateOnFiber, 不会走到render阶段, 那么这个函数自然也就不会再次被调用

我们可以试一下

小总结

先总结一下dispatchSetState这个函数做了什么, 我们传入Set函数的值,其实就是作为action参数传入了dispatchSetState。 然后我们生成了对应的update对象,其中就包含action。 然后将update对象推入栈中, 开启了调度。(如果这里是第一个更新的话的这里还会判断变更前后值是否一致, 一致的话直接不开启调度)

scheduleUpdateOnFiber

这是其实就是开启调度渲染的过程了, 可以看React源码阅读(二)- Scheduler 。 入口函数就是scheduleUpdateOnFiber。感兴趣的可以看看, 这一篇就不会再深究了。 直接聚焦到我们感兴趣的地方

因为整体逻辑较多, 所以我们直接定位两个地方。 既然开启了调度渲染的过程。 那么就会进入render阶段。 感兴趣的可以看React源码阅读(三)- render。 主体逻辑都在这里了。

  • 前面将update推入栈, 那么什么时候将他取出来用呢 ----> 可以找到我们render阶段的入口函数renderRootSync, 在一开始就调用了prepareFreshStack。 该函数又调用了finishQueueingConcurrentUpdates去处理栈中的update对象

  • 进入了render阶段。 那么就会接着再走到我们的beginWork -> renderWithHooks, 就会再调用一次组件函数。 那么此时调用函数中的useState又会发生什么呢?--------->首先要知道我们之前renderWithHooks的时候讲过, 如果是更新阶段的话会挂载上HooksDispatcherOnUpdate。 那么此时我们调用useState实际上就是调用对应的updateState。我们快速定位断点

以下按照上述两点展开

finishQueueingConcurrentUpdates

这个函数的逻辑很清晰。 按照我们入栈的方式, 将update对象, queue对象等从栈中取出来。 然后接着做和enqueueRenderPhaseUpdate一样的操作, 就是将update对象加入queuepending链表

所以我们如果有State的变更, 就会产生对应的update对象。 最后的归宿都是加入的hook.queue.pending链表上 。 isRenderPhaseUpdate影响的是他加入的时机。

js 复制代码
export function finishQueueingConcurrentUpdates(): void {
  const endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;
  concurrentlyUpdatedLanes = NoLanes;
  let i = 0; // 下标从0开始
  while (i < endIndex) {
  ---- 这个过程就是拿到栈中保存的东西而已-----------
    const fiber: Fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const queue: ConcurrentQueue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const update: ConcurrentUpdate = concurrentQueues[i];
    concurrentQueues[i++] = null;
    const lane: Lane = concurrentQueues[i];
    concurrentQueues[i++] = null;
  ---这里的逻辑和上面enqueueRenderPhaseUpdate的逻辑是一模一样的,都是放入链表的操作----
    if (queue !== null && update !== null) {
      const pending = queue.pending; // 又是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;
    }
    ....
  }
}

updateState/updateReducer

可以看到updateState后直接就是调用updateReducer, 所以我们直接走入updateReducer

这里的逻辑看起来也是很长, 我们逐个突破。 这里分成了几部分

第一部分就收首先先拿到hook对象, 然后根据pendingQueuebaseQueue拿到我们的baseQueue

js 复制代码
function updateReducer<S, I, A>(
  reducer: (S, A) => S,
  initialArg: I,
  init?: I => S,
): [S, Dispatch<A>] {
  // 拿到当前的hook对象
  // 该hook对象可能是根据current树的hook生成的, 也可能是直接复用给的。 
  // 具体可以看下面updateWorkInProgressHook内部实现
  const hook = updateWorkInProgressHook();
  const queue = hook.queue;
  // 这里的赋值对useState来说都一样
  // 因为本身我们给queue.lastRenderedReducerc初始化就是basicStateReducer
  // 这里传入的reducer也是basicStateReducer
  queue.lastRenderedReducer = reducer;
  // currentHook在updateWorkInProgressHook内对他进行赋值了
  // 拿到的就是current树上的hook链表
  const current: Hook = (currentHook: any);
  let baseQueue = current.baseQueue;
  // 这里可以看enqueueRenderPhaseUpdate和finishQueueingConcurrentUpdates
  // queue.pending上存放的就是对应的update对象
  const pendingQueue = queue.pending;
  if (pendingQueue !== null) {
    if (baseQueue !== null) { // 初始化的时候还是null
      const baseFirst = baseQueue.next;
      const pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }
    // 当baseQueue为null的处理
    current.baseQueue = baseQueue = pendingQueue;
    queue.pending = null;
  }
  .....
}

baseQueue为空的处理如下。我们假设pendignQueues上链接了四个update对象。其实就是将pendingQueue移到baseQueue

baseQueue不为空的处理如下。 就是将pendingQueuebaseQueue拼接起来

这一部分就是遍历我们的baseQueue, 根据update上的action, 然后拿到的最后的State, 并更新到hook对象上。 这里对代码进行了删减。 关注核心步骤即可

js 复制代码
  if (baseQueue !== null) {
    const first = baseQueue.next;
    let newState = current.baseState;
    let newBaseState = null;
    let newBaseQueueFirst = null;
    let newBaseQueueLast = null;
    let update = first;  // 指针指向baseQueue.next。 其实就是第一个update
    
    // 走一遍update链。 把update上的action都执行完毕, 计算出最后的值, 更新的hook中
    do {
      // 如果优先级不够的话会被打断的。 这里不扩展太多了
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);
      if (shouldSkipUpdate) {
         ....这里直接跳过了
      } else {
       ....
        // 我们在dispatchSetState的时候处理了hasEagerState, 具体可以回看
        // 值已经算好了
        if (update.hasEagerState) {
          // 那么直接赋值
          newState = ((update.eagerState: any): S);
        } else {
         // 还没算的话就从update对象中拿到action。 然后那个期待值
         // 进行赋值
          const action = update.action;
          newState = reducer(newState, action);
        }
      }
      update = update.next;
    } while (update !== null && update !== first);

    ....
    // 还是比较前后的State, 如果一致的话就设置ReceivedUpdate,后续进入Bailout策略
    if (!is(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate();
    }
    // 处理完之后将新的State挂上hook
    hook.memoizedState = newState;
    // 重置baseState和baseQueue
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast;

    queue.lastRenderedState = newState;
  }
  
  if (baseQueue === null) {
    // `queue.lanes` is used for entangling transitions. We can set it back to
    // zero once the queue is empty.
    queue.lanes = NoLanes;
  }
  const dispatch: Dispatch<A> = (queue.dispatch: any);
  return [hook.memoizedState, dispatch];

updateWorkInProgressHook

这里的逻辑虽然长, 但是前面理解过mountWorkInProgressHook, 再来看这个其实也很好理解。 我分成了四个模块

js 复制代码
function updateWorkInProgressHook(): Hook {
  let nextCurrentHook: null | Hook;
  // currentHook是全局维护的变量来的, 初始化为null
  -----
  这个逻辑块处理nextCurrentHook的初始化, 找的是current树
  if (currentHook === null) {
    // 拿到当前的页面使用的Fiber结构
    const current = currentlyRenderingFiber.alternate;
    if (current !== null) {
     // 拿到当前页面使用的Fiber结构的memoizedState。 
     // 上面存放的东西正是我们mountState挂上去的
      nextCurrentHook = current.memoizedState; 
    } else {
     // 如果是首次渲染的话, 此时初始化为null
      nextCurrentHook = null;
    }
  } else {
   // 如果currentHook不为空的话则指向他的next
    nextCurrentHook = currentHook.next;
  }
  ---------------
 这个逻辑课处理的是nextWorkInProgressHook的初始化, 找的是workInProgress树
  let nextWorkInProgressHook: null | Hook;
  if (workInProgressHook === null) {
    nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
  } else {
    nextWorkInProgressHook = workInProgressHook.next;
  }
  --------------
  这里的逻辑块是有可复用的hook, 直接复用即可
   // 当前已经有hook在workInProgress hook上已经有了, 直接复用
  if (nextWorkInProgressHook !== null) {
    workInProgressHook = nextWorkInProgressHook;
    nextWorkInProgressHook = workInProgressHook.next;
    currentHook = nextCurrentHook;
  } else {
  --------------------------
  这里的逻辑块是没有可复用的, 故根据current树上的hook链表生成自己的hook, 并挂上链表
    // 如果没有nextWorkInProgressHook又没有nextCurrentHook可以直接抛出错误了
    // 因为此时不应该会触发update
    if (nextCurrentHook === null) {
      throw new Error('Rendered more hooks than during the previous render.');
    }
    currentHook = nextCurrentHook;
    // 生成新的hook, 用current hook的值进行初始化
    const newHook: Hook = {
      memoizedState: currentHook.memoizedState,
      baseState: currentHook.baseState,
      baseQueue: currentHook.baseQueue,
      queue: currentHook.queue,
      next: null,
    };
    // workInProgressHook还是跟mountWorkInProgressHook中一样, 始终保持指向最后的节点。
    // 如果为空的话说明还没开启挂到memoizedState, 故需要做初始化
    if (workInProgressHook === null) {
      currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
    } else {
      // 不为空的话让新的hook继续挂上链表,让workInProgressHook指向他的next即可
      workInProgressHook = workInProgressHook.next = newHook;
    }
  }
  ------------------
  return workInProgressHook;
}

小总结

我们依赖的是current树上的FibermemoizedState上的hook对象去生成workInProgress树上的Fiberhook对象。 在这个过程中使用workInProgressHook当做指针。 在处理每一个hook对象的时候。 就会找到hookpendingQueue(里面存放着dispatchSetstate触发存入的update对象)。 然后遍历, 根据update上的action获取期待值。 再更新到hook

三、流程图示

我们来一段代码, 然后走一遍流程。 如下图所示有两个useState的用法。 然后写了个点击函数触发了他们更新

js 复制代码
function App() {
  const [count, setCount] = useState(1);
  const [count1 ,setCount1] = useState(() => 3);
  return (  
    <div>
      <div onClick={() => {
        setCount(count + 1); 
        setCount(count + 2);
        setCount(count => count + 3);
        setCount1(count1 + 1);
      }}>{count}</div>
    </div>
  );
}
  • 首先进入页面的时候会进入初次挂载阶段。 在调用该APP()函数的时候, 会顺着useState的顺序形成对应的hook链表(通过mountState函数)。 此时生成的Fiber如图所示。 构造完render后进入commit阶段后相关页面就显示了
  • 接着当我们点击onClick挨个触发setCount的时候, 传入了action, 会根据action等生成update对象。 然后将queueupdate等推入concurrentQueues(通过dispatchSetState)。 这里一共生成了四个update对象。
  • 接着开启调度过程(通过scheduleUpdateOnFiber), 会再次走入render阶段。 然后处理concurrentQueues, 将update对象放入到queuepending链表中。 此时Fiber结构长这样

要注意因为我们dispatchSetState传入的是初次渲染的queue, 故其实我们的操作响应的是current Fiber, 而不是此时render生成的workInProgress

  • 既然再次走入了render过程, 此时又会调用该组件函数, 那么此时调用useState。 此时Fiber节点的初始情况如下
  • 此时调用第一个useState的时候, 走入了updateWorkInProgressHook。 此时nextCurrentHook指向current Fiber的第一个hooknextWorkInProgressHook指向workInProgress Fiber的第一个hook
  • 诶发现nextWorkInProgressHook为空, 那么此时我就根据nextCurrentHook去生成自己的hook对象(唯一不同的是此时next为空)。并且挂上去
  • 此时workInProgressHook指向自己的hook对象了, 此时一看发现我的queue.pending不为空。那不行,我得处理掉他。 首先他是先把pendingQueue直接移到baseQueue。 然后再去处理baseQueue。 这里就是指向的转移而已, 我直接画图
  • 接着就是来处理update对象了, 我们直接聚焦到处理过程就好。 就是通过遍历update链表,更新state。 这里用newState来保持state的上下文
  • 拿到最新的state之后去更新hook即可。 至此,我们对一个useState的处理就结束了。 这里处理完之后呢。 会继续按顺序调用函数的下一个useState, 此时继续走一样的路, 还是根据current Fiber生成新的hook, 然后挂上workInProgresshook链, 让workInProgressHook指向他。 之后都是一样的流程了

至此图示部分就结束了。当然,看源码就知道还有其他情况需要考虑, 这里的图示就讲了主逻辑大概是怎么走的

看懂了你就能理解

  • 为什么函数组件能保持状态
  • hook为什么不能写在判断,循环语句内
  • 调用了Set函数但是state没有变化的情况下为什么函数不会再次被调用
  • useState传参为函数时是如何处理的
  • Set函数传参使用函数为什么能拿到上一次变更的state
  • .....
相关推荐
Redstone Monstrosity15 分钟前
字节二面
前端·面试
东方翱翔22 分钟前
CSS的三种基本选择器
前端·css
Fan_web1 小时前
JavaScript高级——闭包应用-自定义js模块
开发语言·前端·javascript·css·html
yanglamei19621 小时前
基于GIKT深度知识追踪模型的习题推荐系统源代码+数据库+使用说明,后端采用flask,前端采用vue
前端·数据库·flask
千穹凌帝1 小时前
SpinalHDL之结构(二)
开发语言·前端·fpga开发
dot.Net安全矩阵1 小时前
.NET内网实战:通过命令行解密Web.config
前端·学习·安全·web安全·矩阵·.net
Hellc0071 小时前
MacOS升级ruby版本
前端·macos·ruby
前端西瓜哥1 小时前
贝塞尔曲线算法:求贝塞尔曲线和直线的交点
前端·算法
又写了一天BUG1 小时前
npm install安装缓慢及npm更换源
前端·npm·node.js
cc蒲公英2 小时前
Vue2+vue-office/excel 实现在线加载Excel文件预览
前端·vue.js·excel