前置知识

我们知道,函数组件的相关信息都存储在对应的Fiber节点当中,而与函数组件 状态相关的内容 则是存储在Fiber节点的 memoizedState 属性当中。而由于一个函数组件是可以含有 多个状态 的,这就决定了 memoizedState 属性是一个 关于状态信息的集合 ,React使用了 单链表 作为这个集合的数据结构,这个链表的每个节点,我们称之为 hook ,一个 hook 对应着一个状态的信息。而对于每个 hook 来说,一次渲染可能会有多次的更新,这些更新信息存储在 hook.queue.pending 属性当中,这个属性为一个 环链表 ,链表节点称之为 update

上面出现了多个概念,总结一下关系:

  1. Fiber.memoizedState:存储着函数组件的所有状态信息,数据结构为单链表
  2. hook:Fiber.memoizedState单链表的节点,存储着单个状态的信息
  3. hook.queue.pending:为一个环状链表,存储着状态更新的操作,单个节点称为Update

下面是这几个数据结构的详细描述:

  1. type Hook = {
  2. memoizedState: any, // 对应的状态值
  3. baseState: any, // 保存上一轮中断时的状态
  4. baseQueue: Update<any, any> | null, // 保存上一轮中剩余的更新操作
  5. queue: UpdateQueue<any, any> | null, // 保存当前的更新操作
  6. next: Hook | null, // 指向下一个hook
  7. };
  8. type Update<S, A> = {|
  9. lane: Lane, // 优先级相关,暂时忽略
  10. action: A, // 更新操作或者数据
  11. eagerReducer: ((S, A) => S) | null,
  12. eagerState: S | null,
  13. next: Update<S, A>,
  14. priority?: ReactPriorityLevel, // 优先级相关,暂时忽略
  15. |};
  16. type UpdateQueue<S, A> = {|
  17. pending: Update<S, A> | null, // Update的环状链表
  18. dispatch: (A => mixed) | null, // 对应的dispatcher
  19. lastRenderedReducer: ((S, A) => S) | null, // 保存着上次渲染的reducer
  20. lastRenderedState: S | null, // 优化相关,保存着上一次渲染的state
  21. |};

了解了上面几个概念后,我们分别从mount阶段和update阶段来理解useState的运行流程

mount阶段

image.png
上面是useState的调用流程图:

useState的调用是在render阶段,其调用从本函数组件的Fiber节点的 beginWork 开始,随后进入 mountIndeterminateComponent ,此函数会同时处理类组件和函数组件的相关操作,然后通过 renderWithHooks 调用 Component 即函数组件本身得到JSX节点。

useState函数会通过调用 dispatcher.useState 进入到真正的useState流程当中,这一步的目的是建立一个中间层,根据不同的情况(挂载,更新等等)对应着不同的dispatcher,从而获取不同的useState函数。

来到 mountState 中就是mount阶段的 核心逻辑 ,这部分主要做了两件事:

  1. 生成当前状态对应的hook,并挂载到workInProgress Fiber节点的 memoizedState 链表上
  2. 返回状态(initialState)和dispatcher(用于更新状态,后面会介绍)数组

而对于 useReducer 来说,mount阶段的核心逻辑跟useState几乎一致,不同的只不过是在计算初始值的时候可能需要调用一下初始化函数。

update阶段

image.png
前面部分与mount阶段大致相同,可以看到updateState不过是调用了 updateReducer(baseStateReducer ,也就是说更新阶段的useState是一个特殊的updateReducer。

因此这一部分的核心逻辑在函数updateReducer里面,主要操作可以概括为三步:

  1. 获取当前状态的hook( updateWorkInProgressHook )
  2. 根据hook的内容得到最新的状态
  3. 返回state和dispatcher

    updateWorkInProgressHook

    顾名思义,此函数的目的是 更新workInProgressHook

此阶段中current Fiber中的hook链表(memoizedState)是 完整的 ,因为它是在上一个render阶段构建的,而workInProgress Fiber的hook链表是 不完整的 ,需要依托current Fiber的链表进行构建,最终的构建结果两条hook链表 结构一致这也是为什么React不允许useState放在条件语句等地方,因为需要保证两条hook链表的结构的一致性。

因此更新workInProgressHook的方法就是 通过获取current Fiber的下一个hook来构建workInProgress Fiber的下一个hook,并将此hook赋值给workInProgressHook

由此对应着四个变量

  • currentHook:current Fiber 当中与workInProgressHook(未被更新)对应的hook
  • nextCurrentHook:current FIber 中的下一个hook
  • workInProgressHook:在此函数中等待被更新
  • nextWorkInProgressHook:workInProgress Fiber中的下一个hook

一般情况下,nextWorkInProgressHook是不存在的,因为还没被构建出来,但是如果是 在render阶段进行的更新,那么就有可能出现复用之前构建好的workInProgressHook的情况 ,譬如以下情况:**

  1. function App() {
  2. const [num, setNum] = useState(0)
  3. const onClick = () => {
  4. setNum(state => state + 1)
  5. }
  6. setNum(state => state + 1)
  7. return (
  8. <div className="App" onClick={onClick}>
  9. {num}
  10. </div>
  11. );
  12. }

对于不是复用的情况,则会根据currentHook来新建workInProgressHook:

  1. const newHook: Hook = {
  2. memoizedState: currentHook.memoizedState,
  3. baseState: currentHook.baseState,
  4. baseQueue: currentHook.baseQueue,
  5. queue: currentHook.queue,
  6. next: null,
  7. };

总结一下,更新workInProgressHook分为三步:

  1. 获取current Fiber的下一个hook(nextCurrentHook)
  2. 获取workInProgressHook的下一个Hook(nextWorkInProgressHook)
  3. 判断nextWorkInProgressHook是否存在

    1. 如果存在,那么直接复用
    2. 如果不存在,则根据nextCurrentHook构建nextWorkInProgressHook

      updateReducer

      获取到当前hook以后,则需要根据此hook来更新对应的状态了,更新状态可以总结为以下两步:
  4. 将上一轮 未完成的update(baseQueue)当前新增的update(queue.pending) 合并

  5. 逐个遍历链表的每个update的action,获得最终的结果

备注:上述步骤先不考虑调度相关,只从完全的状态更新出发

其中上述的合并是 两个环链表的合并 ,相关逻辑如下:

  1. const baseFirst = baseQueue.next; // baseQueue的头部
  2. const pendingFirst = pendingQueue.next; // pendingQueue的头部
  3. baseQueue.next = pendingFirst; // baseQueue的尾部指向pendingQueue的头部
  4. pendingQueue.next = baseFirst; // pendingQueue的尾部指向baseQueue的头部

遍历的逻辑如下(忽略调度相关的):

  1. do {
  2. if (update.eagerReducer === reducer) {
  3. // 优化相关
  4. // If this update was processed eagerly, and its reducer matches the
  5. // current reducer, we can use the eagerly computed state.
  6. newState = ((update.eagerState: any): S);
  7. } else {
  8. const action = update.action;
  9. newState = reducer(newState, action);
  10. }
  11. update = update.next;
  12. } while (update !== null && update !== first);

其中重点说一下上面的 eagerState ,在dispatchAction时,如果当前的hook的queue为空,那么就会提前计算当前Update的结果,如果与之前保存的结果(queue.lastRenderedState)一致的话就不会进入到更新程序直接返回,避免不必要的更新。上面的情况是结果不一致,但是可以复用在dispatchAction中已经计算好的结果。

  1. const currentState: S = (queue.lastRenderedState: any);
  2. const eagerState = lastRenderedReducer(currentState, action);
  3. update.eagerReducer = lastRenderedReducer;
  4. update.eagerState = eagerState;
  5. if (is(eagerState, currentState)) {
  6. return;
  7. }

其次可以看到上述的状态更新使用的是 reducer(newState, action) ,对于useState来说,此reducer是 baseStateReducer ,对于useReducer来说则为用户自定义的reducer。baseStateReducer的内容如下:

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

逻辑非常简单,如果action是函数,则传入state进行调用,否则直接返回action的值。

dispatchAction

此函数的定义如下:

  1. function dispatchAction<S, A>(
  2. fiber: Fiber,
  3. queue: UpdateQueue<S, A>,
  4. action: A,
  5. ) {}

上面说到无论mount阶段还是update阶段都会返回一个dispatcher,这个dispatcher就是一个 绑定了fiber和状态hook对应的updateQueue的dispatchAction函数

dispatchAction的操作分为两步:

  1. 根据传入的action 构建Update对象
  2. 将此Update对象 拼接 到queue.pending环链表当中
  3. 就是上面提到过的相关性能优化工作
  4. 调度fiber进行更新