React Source Code

    7. 函数式组件-构建细节以及更新(中)

    Published
    November 20, 2022
    Reading Time
    4 min read
    Author
    Felix

    在上一篇文章中,我们讨论了函数式组件的构建和更新过程。而本文将深入探讨React hooks的工作原理。

    首先,让我们理解一下hooks的分类:

    1. 状态Hook (State Hook): 如useStateuseReducer
    2. 副作用Hook (Effect Hook): 如useEffectuseLayoutEffect
    3. Ref Hook: 如useRef

    而本文将讲状态Hook,useStateuseReducer的实现。

    2. useState的实现

    useState是React中最常用的Hook之一。它的实现分为两个主要函数:mountState(用于初始化)和updateState(用于更新)。让我们先来看mountState的实现。

    2.1 mountState

    function mountState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      const hook = mountWorkInProgressHook();
    
      if (typeof initialState === 'function') {
        initialState = initialState();
      }
    
      hook.memoizedState = hook.baseState = initialState;
    
      const queue = (hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: (initialState: any),
      });
    
      const dispatch: Dispatch<
        BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
    
      return [hook.memoizedState, dispatch];
    }
    

    让我们逐行分析这个函数:

    1. const hook = mountWorkInProgressHook(); 这行代码创建了一个新的hook对象,并将其添加到当前fiber的hooks链表中。这是React管理多个hooks的关键机制。

    2. if (typeof initialState === 'function') {
        initialState = initialState()
      }
      

      这是一个优化,允许用户传入一个函数来计算初始状态。这对于复杂或昂贵的初始状态计算很有用,因为它只会在组件挂载时执行一次。

    3. hook.memoizedState = hook.baseState = initialState; 这里初始化了hook的memoizedStatebaseStatememoizedState存储当前状态,而baseState用于在更新过程中计算新状态。

    4. const queue = (hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: basicStateReducer,
        lastRenderedState: (initialState: any),
      });
      

      这创建了一个更新队列。pending用于存储待处理的更新,dispatch是更新函数,lastRenderedReducer是最后一次渲染使用的reducer(对于useState,这是一个基本的状态更新函数),lastRenderedState是最后一次渲染的状态。

    5. const dispatch: Dispatch<
        BasicStateAction<S>,
      > = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
      

      这创建了dispatch函数。它实际上是dispatchAction函数的绑定版本,预先绑定了当前fiber和更新队列。这是一个优化,避免了每次调用dispatch时都要重新绑定这些参数。

    6. return [hook.memoizedState, dispatch]; 最后返回当前状态和dispatch函数,这就是我们在使用useState时得到的数组。

    2.2 dispatchAction

    接下来,让我们看看dispatchAction函数,这是状态更新的核心:

    function dispatchAction<S, A>(
      fiber: Fiber,
      queue: UpdateQueue<S, A>,
      action: A,
    ) {
      const eventTime = requestEventTime();
      const lane = requestUpdateLane(fiber);
    
      const update: Update<S, A> = {
        lane,
        action,
        eagerReducer: null,
        eagerState: null,
        next: (null: any),
      };
    
      const pending = queue.pending;
      if (pending === null) {
        update.next = update;
      } else {
        update.next = pending.next;
        pending.next = update;
      }
      queue.pending = update;
    
      const alternate = fiber.alternate;
      if (
        fiber === currentlyRenderingFiber ||
        (alternate !== null && alternate === currentlyRenderingFiber)
      ) {
        didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true;
      } else {
        if (
          fiber.lanes === NoLanes &&
          (alternate === null || alternate.lanes === NoLanes)
        ) {
          const lastRenderedReducer = queue.lastRenderedReducer;
          if (lastRenderedReducer !== null) {
            let prevDispatcher;
            try {
              const currentState: S = (queue.lastRenderedState: any);
              const eagerState = lastRenderedReducer(currentState, action);
              update.eagerReducer = lastRenderedReducer;
              update.eagerState = eagerState;
              if (is(eagerState, currentState)) {
                return;
              }
            } catch (error) {
              // Suppress the error. It will throw again in the render phase.
            }
          }
        }
        scheduleUpdateOnFiber(fiber, lane, eventTime);
      }
    }
    

    让我们逐步分析这个函数:

    1. const eventTime = requestEventTime()
      const lane = requestUpdateLane(fiber)
      

      这两行代码获取了当前的事件时间和更新优先级(lane)。React使用lane模型来管理更新的优先级,这是一个重要的优化机制。

    2. const update: Update<S, A> = {
        lane,
        action,
        eagerReducer: null,
        eagerState: null,
        next: (null: any),
      };
      

      这创建了一个新的更新对象。lane表示更新的优先级,action是状态更新的动作,eagerReducereagerState用于优化,next用于链接到下一个更新。

    3. const pending = queue.pending
      if (pending === null) {
        update.next = update
      } else {
        update.next = pending.next
        pending.next = update
      }
      queue.pending = update
      

      这段代码将新的更新添加到更新队列中。注意这里使用了环形链表的结构,这使得React可以高效地处理多个更新。

    4. if (fiber === currentlyRenderingFiber || (alternate !== null && alternate === currentlyRenderingFiber)) {
        didScheduleRenderPhaseUpdateDuringThisPass = didScheduleRenderPhaseUpdate = true
      } else {
        // ... (优化逻辑)
      }
      

      这段代码检查是否在渲染阶段调度了更新。如果是,它会设置一个标志,以便React可以在当前渲染完成后立即开始新的渲染。

    5. if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
        // ... (优化逻辑)
      }
      

      这是一个重要的优化。如果当前fiber没有待处理的更新,React会尝试立即计算新的状态。

    6. const lastRenderedReducer = queue.lastRenderedReducer
      if (lastRenderedReducer !== null) {
        // ... (优化逻辑)
      }
      

      这里React尝试使用上一次渲染的reducer来计算新状态。这是一个优化,可以避免不必要的重新渲染。

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

      这段代码计算了新的状态(eagerState)。如果新状态与当前状态相同,函数直接返回,避免了不必要的更新。

    8. scheduleUpdateOnFiber(fiber, lane, eventTime); 如果需要更新,这行代码会调度一个更新。

    2.3 updateState

    当组件更新时,会调用updateState。这个函数实际上是调用了updateReducer:

    function updateState<S>(
      initialState: (() => S) | S,
    ): [S, Dispatch<BasicStateAction<S>>] {
      return updateReducer(basicStateReducer, (initialState: any));
    }
    

    updateReducer是一个复杂的函数,它处理了状态的实际更新。让我们看看它的关键部分:

    function updateReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = updateWorkInProgressHook();
      const queue = hook.queue;
    
      queue.lastRenderedReducer = reducer;
    
      // ... (处理baseQueue和pendingQueue)
    
      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 = update.lane;
          if (!isSubsetOfLanes(renderLanes, updateLane)) {
            // 优先级不够,跳过这个更新
            const clone: Update<S, A> = {
              lane: updateLane,
              action: update.action,
              eagerReducer: update.eagerReducer,
              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,
            );
          } else {
            // 优先级足够,应用这个更新
            if (newBaseQueueLast !== null) {
              const clone: Update<S, A> = {
                lane: NoLane,
                action: update.action,
                eagerReducer: update.eagerReducer,
                eagerState: update.eagerState,
                next: (null: any),
              };
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
    
            if (update.eagerReducer === reducer) {
              // 如果有缓存的计算结果,直接使用
              newState = ((update.eagerState: any): S);
            } else {
              // 否则,调用reducer计算新状态
              const action = update.action;
              newState = reducer(newState, action);
            }
          }
          update = update.next;
        } while (update !== null && update !== first);
    
        // ... (更新hook的状态)
      }
    
      const dispatch: Dispatch<A> = (queue.dispatch: any);
      return [hook.memoizedState, dispatch];
    }
    

    这个函数的核心是一个循环,它遍历所有待处理的更新。让我们逐步分析:

    1. const hook = updateWorkInProgressHook(); 这行代码获取当前正在工作的hook。React使用链表结构来管理一个组件中的多个hooks,这允许hooks在渲染过程中保持稳定的顺序。

    2. queue.lastRenderedReducer = reducer; 更新队列中存储的最后一次渲染使用的reducer。这是为了后续的优化。

    3. 接下来的大循环是整个函数的核心,它遍历所有待处理的更新。

    4. if (!isSubsetOfLanes(renderLanes, updateLane)) { ... } 这个条件检查当前更新的优先级是否足够高。如果优先级不够,这个更新会被跳过,并被添加到一个新的队列中,等待下次渲染。

    5. if (update.eagerReducer === reducer) { ... } 这是一个重要的优化。如果当前更新已经有了预计算的结果(eagerState),并且使用的reducer没有变,那么就直接使用预计算的结果,避免重复计算。

    6. newState = reducer(newState, action); 如果没有预计算的结果,就调用reducer计算新的状态。

    这个函数展示了React在状态更新过程中的几个关键优化:

    • 优先级调度:通过lane模型,React可以跳过低优先级的更新,优先处理高优先级的更新。
    • 批量更新:多个更新被放在一个队列中一起处理,提高了效率。
    • 预计算:通过eagerReducer和eagerState,React可以在某些情况下避免重复计算。

    3. useReducer的实现

    useReducer的实现与useState非常相似。主要区别在于,useReducer允许用户提供自定义的reducer函数。让我们看看mountReducer的实现:

    function mountReducer<S, I, A>(
      reducer: (S, A) => S,
      initialArg: I,
      init?: I => S,
    ): [S, Dispatch<A>] {
      const hook = mountWorkInProgressHook();
      let initialState;
      if (init !== undefined) {
        initialState = init(initialArg);
      } else {
        initialState = ((initialArg: any): S);
      }
      hook.memoizedState = hook.baseState = initialState;
      const queue = (hook.queue = {
        pending: null,
        dispatch: null,
        lastRenderedReducer: reducer,
        lastRenderedState: (initialState: any),
      });
      const dispatch: Dispatch<A> = (queue.dispatch = (dispatchAction.bind(
        null,
        currentlyRenderingFiber,
        queue,
      ): any));
      return [hook.memoizedState, dispatch];
    }
    

    这个函数与mountState非常相似,主要区别在于:

    1. 它接受一个reducer函数作为参数。
    2. 它允许通过init函数来初始化状态,这提供了更灵活的初始化方式。

    useReducer的更新过程(updateReducer)与useState完全相同,我们在前面已经详细分析过了。

    4. 深入思考

    1. Hooks的本质:Hooks本质上是一种状态管理的方式。它们允许函数组件拥有自己的状态,而不需要转换为类组件。这大大简化了React组件的编写。

    2. 链表结构:React使用链表来管理hooks,这允许hooks在多次渲染之间保持稳定的顺序。这也是为什么hooks不能在条件语句中使用的原因。

    3. 批量更新和优先级:React的更新机制非常精细。它可以将多个更新批量处理,并且可以根据优先级来决定哪些更新应该先被处理。这大大提高了应用的性能和响应速度。

    4. 懒初始化useStateuseReducer都支持懒初始化,即可以传入一个函数来计算初始状态。这对于复杂的初始状态计算很有用,可以避免在每次渲染时都重新计算。

    5. 闭包陷阱:Hooks的实现依赖于JavaScript的闭包机制。这可能导致一些难以察觉的bug,比如在异步操作中使用旧的状态。理解这一点对于正确使用hooks非常重要。