web前端技术分享
useState_useState相关实现细节

一、每次setState触发渲染,我们所定义的组件函数都会被react执行一次

以这段代码为例:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 function App() {
  const [state, setState] = useState(0);

  console.log(state)
  const onclick = () => {
    setState(state + 1)
  }
  
  return (
    <div className='div' onClick={onclick}>
      {state}
    </div>

  );
}

你每次点击一次div,就会触发onclick函数去更新状态变量。同时你会发现每次状态更新,第4行的console.log都会被执行。上一节我们讲到performConcurrentWorkOnRoot是开始执行更新任务,我们从performConcurrentWorkOnRoot继续往后看: performConcurrentWorkOnRoot->renderRootConcurrent->workLoopConcurrent->performUnitOfWork->beginWork(位于packages\react-reconciler\src\ReactFiberBeginWork.js)->updateFunctionComponent->renderWithHooks->Component。 沿着函数调用链至此,你可以看到,它又执行了一次我们所定义的组件函数。 不是说react就是为了使得函数每次更新都执行一次。而是每次更新都需要依赖去执行一次这个函数来做状态变量得更改,来解析jsx结构计算更新后的组件结构。而函数每次都执行,就引入了useCallBack,useMemo这两个hook,因为常规来讲这个例子中的click方法,也是每次都重新创建赋值的,会耗费性能。state变量其实也是每次重新执行然后重新定义的值。上一次执行的函数空间就被销毁了

二、const [state, setState] = useState(initialState);状态变量state在调用setState之后,会触发render任务,在render过程中才去更改状态变量state的值。

在useState调用栈中我们知道,在调用setState时实际最终是触发了调度器去调度performConcurrentWorkOnRoot任务渲染。当我们把任务交给调度器时,这个任务便会在何时执行便脱离了原来的代码执行流程,由调度器去确定执行时机,所以当我们在一个js任务中调用了setState之后,在这段代码之后去获取状态变量state的值并不是setState之后的值。因为这时,render过程去更新fiber树,commit渲染ui结果的过程并没有发生。只有当调度器调度执行了更新fiber的任务之后,state才被更改。fiber你暂且可以看作是一颗虚拟Dom树,树的每个节点都存放着对应组件的相关信息,包括state。因此你会发现setState之后state的值并没有立即被更改。如这段react样例代码:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 function App() {
  const [state, setState] = useState(0);

  const onclick = () => {
    setState(1)
    console.log(state)
  }

  return (
    <div className='div' onClick={onclick}>
       测试一下
    </div>

  );
}

state初始值为0,点击一下div组件触发onclick方法。setState(1)将状态state改为1,但是console.log打印结果依然为0。这个样例非常简单,你可以自己尝试一下。如果你有看过useState函数调用栈的章节,你应该知道: setState 就相当于 dispatchSetState.bind( null, currentlyRenderingFiber, queue, )。 dispatchSetState方法的实现源码位于packages\react-reconciler\src\ReactFiberHooks.js。这里setState的传参就对应dispatchSetState方法的第三个参数,它会将要更新的值加入更新队列中,等待调度。 state的新值的获得是react如(一)中所说,在重新执行我们的组件函数时,在const [state, setState] = useState(0);这行重新获得的。 其实从某种意义上来讲,这次得state跟上一次得state其实是完全处在两个不同得函数作用域的两个变量。更新后的那个状态是react重新执行的组件函数中定义的新的值。而不是说我把上一变量修改为新的变量了。上一次的函数空间包括里面的各种变量,函数都已经被完全销毁了。所有的都是新执行生成的。可以看这个例子:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 function App() {
  const [state, setState] = useState(0);

  const onclick = () => {
    setState(10)
    setTimeout(() => {
      console.log('setTimeout', state)
    }, 2000);
  }

  useEffect(() => {
    console.log('useEffect', state)
  }, [state])
  return (
    <div className='div' onClick={onclick}>
      <p></p>{state}
    </div>

  );
}

控制台打印结果如下: image.png useEffect副作用会在首次执行时调用一次此时state为0。然后点击div触发click时间,重新setState。组件重新刷新,出发副作用,打印刷新后的state为10。但是setTimeout在2s后打印结果依然为0,这是因为这里打印的state是第一次组件函数调用时定义的静态变量state。你setState之后,又一次重新执行了这个函数,重新定义了新的state变量。而setTimeout是在第一次执行中组件函数的函数作用域中使用的,console.log打印的state变量是第一执行的组件函数中的state变量。 总结:setState看起来像是异步的,你不能直接setState之后就拿到新的state值,而是在调度更新时才能拿到。实际调度更新过程中,新值得获取是在渲染过程重新调用组件函数时获得的。而且已经是一个全新的函数作用域了。

三、一次任务中多次setState,setState会被进行合并。

同样也因为fiber更新并不发生在调用setState的任务中。那么当你在一次任务中更新多次同一个state,实际上只有最后一次setState会真正被应用到fiber中。所以你会看到一个现象,多次setState同一个state会被合并为最后一次setState。如下面这段示例:

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 function App() {
  const [state, setState] = useState(0);

  console.log(state)
  const onclick = () => {
    for (let i = 0; i < 10; i++) {
      setState(i)
    }
  }

  return (
    <div className='div' onClick={onclick}>
      <p></p>{state}
    </div>

  );
}

当你触发click时间时,调用了10次setState,但实际你会注意到console.log只打印了一次9。也就是在一次同步任务中调用多次setState这些中间状态会被合并,只执行最后一次。 没错,看起来是这样,但实际上setState既然调用了,肯定是要执行的。只是这些更新都被放在了hook的状态队列中。在进行更新时,会进行合并。这个合并就发生在源码的updateState方法,也就是更新时useState的实际方法。沿着updateState->updateReducer->updateReducerImpl的调用链。在updateReducerImpl方法中有一段do...while()的循环体,就是在进行队列合并。

  1 
  2 
  3 
  4 
  5 
  6 
  7 
  8 
  9 
 10 
 11 
 12 
 13 
 14 
 15 
 16 
 17 
 18 
 19 
 20 
 21 
 22 
 23 
 24 
 25 
 26 
 27 
 28 
 29 
 30 
 31 
 32 
 33 
 34 
 35 
 36 
 37 
 38 
 39 
 40 
 41 
 42 
 43 
 44 
 45 
 46 
 47 
 48 
 49 
 50 
 51 
 52 
 53 
 54 
 55 
 56 
 57 
 58 
 59 
 60 
 61 
 62 
 63 
 64 
 65 
 66 
 67 
 68 
 69 
 70 
 71 
 72 
 73 
 74 
 75 
 76 
 77 
 78 
 79 
 80 
 81 
 82 
 83 
 84 
 85 
 86 
 87 
 88 
 89 
 90 
 91 
 92 
 93 
 94 
 95 
 96 
 97 
 98 
 99 
100 
101 
102 
103 
104 
105 
106 
107 
108 
109 
110 
111 
112 
113 
114 
115 
116 
117 
118 
119 
120 
121 
122 
123 do {
      // An extra OffscreenLane bit is added to updates that were made to
      // a hidden tree, so that we can distinguish them from updates that were
      // already there when the tree was hidden.
      const updateLane = removeLanes(update.lane, OffscreenLane);
      const isHiddenUpdate = updateLane !== update.lane;

      // Check if this update was made while the tree was hidden. If so, then
      // it's not a "base" update and we should disregard the extra base lanes
      // that were added to renderLanes when we entered the Offscreen tree.
      const shouldSkipUpdate = isHiddenUpdate
        ? !isSubsetOfLanes(getWorkInProgressRootRenderLanes(), updateLane)
        : !isSubsetOfLanes(renderLanes, updateLane);

      if (shouldSkipUpdate) {
        // Priority is insufficient. Skip this update. If this is the first
        // skipped update, the previous update/state is the new base
        // update/state.
        const clone: Update<S, A> = {
          lane: updateLane,
          revertLane: update.revertLane,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: (null: any),
        };
        if (newBaseQueueLast === null) {
          newBaseQueueFirst = newBaseQueueLast = clone;
          newBaseState = newState;
        } else {
          newBaseQueueLast = newBaseQueueLast.next = clone;
        }
        // Update the remaining priority in the queue.
        // TODO: Don't need to accumulate this. Instead, we can remove
        // renderLanes from the original lanes.
        currentlyRenderingFiber.lanes = mergeLanes(
          currentlyRenderingFiber.lanes,
          updateLane,
        );
        markSkippedUpdateLanes(updateLane);
      } else {
        // This update does have sufficient priority.

        // Check if this is an optimistic update.
        const revertLane = update.revertLane;
        if (!enableAsyncActions || revertLane === NoLane) {
          // This is not an optimistic update, and we're going to apply it now.
          // But, if there were earlier updates that were skipped, we need to
          // leave this update in the queue so it can be rebased later.
          if (newBaseQueueLast !== null) {
            const clone: Update<S, A> = {
              // This update is going to be committed so we never want uncommit
              // it. Using NoLane works because 0 is a subset of all bitmasks, so
              // this will never be skipped by the check above.
              lane: NoLane,
              revertLane: NoLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            newBaseQueueLast = newBaseQueueLast.next = clone;
          }
        } else {
          // This is an optimistic update. If the "revert" priority is
          // sufficient, don't apply the update. Otherwise, apply the update,
          // but leave it in the queue so it can be either reverted or
          // rebased in a subsequent render.
          if (isSubsetOfLanes(renderLanes, revertLane)) {
            // The transition that this optimistic update is associated with
            // has finished. Pretend the update doesn't exist by skipping
            // over it.
            update = update.next;
            continue;
          } else {
            const clone: Update<S, A> = {
              // Once we commit an optimistic update, we shouldn't uncommit it
              // until the transition it is associated with has finished
              // (represented by revertLane). Using NoLane here works because 0
              // is a subset of all bitmasks, so this will never be skipped by
              // the check above.
              lane: NoLane,
              // Reuse the same revertLane so we know when the transition
              // has finished.
              revertLane: update.revertLane,
              action: update.action,
              hasEagerState: update.hasEagerState,
              eagerState: update.eagerState,
              next: (null: any),
            };
            if (newBaseQueueLast === null) {
              newBaseQueueFirst = newBaseQueueLast = clone;
              newBaseState = newState;
            } else {
              newBaseQueueLast = newBaseQueueLast.next = clone;
            }
            // Update the remaining priority in the queue.
            // TODO: Don't need to accumulate this. Instead, we can remove
            // renderLanes from the original lanes.
            currentlyRenderingFiber.lanes = mergeLanes(
              currentlyRenderingFiber.lanes,
              revertLane,
            );
            markSkippedUpdateLanes(revertLane);
          }
        }

        // Process this update.
        const action = update.action;
        if (shouldDoubleInvokeUserFnsInHooksDEV) {
          reducer(newState, action);
        }
        if (update.hasEagerState) {
          // If this update is a state update (not a reducer) and was processed eagerly,
          // we can use the eagerly computed state
          newState = ((update.eagerState: any): S);
        } else {
          newState = reducer(newState, action);
        }
      }
      console.log('update', update)
      update = update.next;
    } while (update !== null && update !== first);

整个合并流程还涉及到一些优先级之类的逻辑,暂不展开。主要知道这个循环就是遍历状态队列,然后进行合并,得到这一次要真实渲染的状态。而不是每一次的setState都会被真实渲染。

四、setState的同异步

在学界流传着一个说法,setState在原生事件和在setTimeout等脱离react上下文的代码中中是同步的,其它情况是异步。这个说法是对的,但是过于笼统和不准确。 首先react存在legacy和concurrent两种模式。setState也有类组件中的this.setState和hook中的setState。这些情况之间是存在差异的。 “setState在原生事件和在setTimeout等脱离react上下文的代码中中是同步的,其它情况是异步的"。这种说法仅对legacy模式下类组件的this.setState有效。 其余情况均为异步。