react18 state的更新

302 阅读2分钟

先来说this.setSate的更新 这是一个老掉牙的问题,我们都知道,在legacy模式下,合成事件中有batchedEventUpdates方法,所以会开启批量更新,所以会在感觉上是异步的。但是如果在异步回调中调用会丢失上下文,进入同步调用的逻辑。

        setTimeout(() => {
          this.setState({
           count: this.state.count + 1
          })
          console.log(this.state.count);
          this.setState({
              count: this.state.count + 1
          })
          console.log(this.state.count);
          this.setState({
              count: this.state.count + 1
          })
          console.log(this.state.count);
      })

但是在新版本react18中concurrent不管是否有上下文,都是异步执行的。本文主要讨论concurrent模式下,this.setState是如何更新的。

首先调用setState会执行enqueueSetState

image.png 然后会调用scheduleUpdateOnFiber开始更新。接着会调用ensureRootIsScheduled来判断以什么优先级去更新。

image.png 可以看到第一个this.setState会以NormalPriority来调度整棵树(performConcurrentWorkOnRoot)。并且将root.callbackPriority = newCallbackPriority;

我们来看第二次this.setState。

image.png

这一次直接return了,不去调度,因为本次的优先级和上次的优先级一致,所以直接退出了。

这样就可以解释为什么setState在concurrent模式下是异步的,因为只会执行一次performConcurrentWorkOnRoot来调度更新,之后因为优先级是一致的会return掉。 performConcurrentWorkOnRoot会调度一个宏任务来延迟执行(MessageChannel)。

我们再来看看this.setState的更新流程,和如何合并state状态的:

image.png

调用链条enqueueUpdate$1->enqueueConcurrentClassUpdate->enqueueUpdate

image.png

最后维护了concurrentQueues队列

function finishQueueingConcurrentUpdates() {
  var endIndex = concurrentQueuesIndex;
  concurrentQueuesIndex = 0;
  concurrentlyUpdatedLanes = NoLanes;
  var i = 0;

  while (i < endIndex) {
    var fiber = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var queue = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var update = concurrentQueues[i];
    concurrentQueues[i++] = null;
    var lane = concurrentQueues[i];
    concurrentQueues[i++] = null;

    if (queue !== null && update !== null) {
      var 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;
    }

    if (lane !== NoLane) {
      markUpdateLaneFromFiberToRoot(fiber, update, lane);
    }
  }
}

然后维护了环状链表。在更新阶段去获取最新的state. 最后在performUnitOfWork中调用updateClassInstance中的processUpdateQueue方法去获取最新的状态 他会遍历环状链表,合并状态。

image.png

合并的逻辑在getStateFromUpdate中

function getStateFromUpdate(workInProgress, queue, update, prevState, nextProps, instance) {
 switch (update.tag) {
   case ReplaceState:
     {
       var payload = update.payload;

       if (typeof payload === 'function') {
         // Updater function
         {
           enterDisallowedContextReadInDEV();
         }

         var nextState = payload.call(instance, prevState, nextProps);

         {
           if ( workInProgress.mode & StrictLegacyMode) {
             setIsStrictModeForDevtools(true);

             try {
               payload.call(instance, prevState, nextProps);
             } finally {
               setIsStrictModeForDevtools(false);
             }
           }

           exitDisallowedContextReadInDEV();
         }

         return nextState;
       } // State object


       return payload;
     }

   case CaptureUpdate:
     {
       workInProgress.flags = workInProgress.flags & ~ShouldCapture | DidCapture;
     }
   // Intentional fallthrough

   case UpdateState:
     {
       var _payload = update.payload;
       var partialState;

       if (typeof _payload === 'function') {
         // Updater function
         {
           enterDisallowedContextReadInDEV();
         }

         partialState = _payload.call(instance, prevState, nextProps);

         {
           if ( workInProgress.mode & StrictLegacyMode) {
             setIsStrictModeForDevtools(true);

             try {
               _payload.call(instance, prevState, nextProps);
             } finally {
               setIsStrictModeForDevtools(false);
             }
           }

           exitDisallowedContextReadInDEV();
         }
       } else {
         // Partial state object
         partialState = _payload;
       }

       if (partialState === null || partialState === undefined) {
         // Null and undefined are treated as no-ops.
         return prevState;
       } // Merge the partial state and the previous state.


       return assign({}, prevState, partialState);
     }

   case ForceUpdate:
     {
       hasForceUpdate = true;
       return prevState;
     }
 }

 return prevState;
}

我们继续讨论useState是如何更新的

import React, { useState } from 'react'

const Index = () => {

   const [count, setCount] = useState(1)

   const handleClick = () => {
       setCount(count + 1)
       setCount(count + 1)
       setCount(count + 1)
   }

   return <div onClick={handleClick}>{count}</div>

}

调用setCount进入dispatchSetState,和this.setState不同的是,进入dispatchSetState会浅比较两次state的值

function dispatchSetState(fiber, queue, action) {
 {
   if (typeof arguments[3] === 'function') {
     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().');
   }
 }

 var lane = requestUpdateLane(fiber);
 var update = {
   lane: lane,
   action: action,
   hasEagerState: false,
   eagerState: null,
   next: null
 };

 if (isRenderPhaseUpdate(fiber)) {
   enqueueRenderPhaseUpdate(queue, update);
 } else {
   var alternate = fiber.alternate;

   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.
     var lastRenderedReducer = queue.lastRenderedReducer;

     if (lastRenderedReducer !== null) {
       var prevDispatcher;

       {
         prevDispatcher = ReactCurrentDispatcher$1.current;
         ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;
       }

       try {
         var currentState = queue.lastRenderedState;
         var 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.hasEagerState = true;
         update.eagerState = eagerState;
        //浅比较两次值如果相同则退出
         if (objectIs(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.
           // TODO: Do we still need to entangle transitions in this case?
           enqueueConcurrentHookUpdateAndEagerlyBailout(fiber, queue, update);
           return;
         }
       } catch (error) {// Suppress the error. It will throw again in the render phase.
       } finally {
         {
           ReactCurrentDispatcher$1.current = prevDispatcher;
         }
       }
     }
   }

   var root = enqueueConcurrentHookUpdate(fiber, queue, update, lane);

   if (root !== null) {
     var eventTime = requestEventTime();
     scheduleUpdateOnFiber(root, fiber, lane, eventTime);
     entangleTransitionUpdate(root, queue, lane);
   }
 }

 markUpdateInDevTools(fiber, lane);
}

可以看到,浅比较之后进入了enqueueConcurrentHookUpdate

function enqueueConcurrentHookUpdate(fiber, queue, update, lane) {
 var concurrentQueue = queue;
 var concurrentUpdate = update;
 enqueueUpdate(fiber, concurrentQueue, concurrentUpdate, lane);
 return getRootForUpdatedFiber(fiber);
}

调用了enqueueUpdate和this.setState调用相同维护concurrentQueues队列之后在调用perfomUnitWork前会调用finishQueueingConcurrentUpdates,形成环状链表。

接着就是调用scheduleUpdateOnFiber从根节点调度更新了。之后的流程是一样,包括第二次调用因为优先级相同会直接return。