3.5|【进阶】深入源码理解 useState

433 阅读7分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天,点击查看活动详情

如果你想要学习怎样使用 useState,可以看上一篇👉 02|在 React 中正确使用 useState 的姿势

这一篇是作为上一篇的补充,主要是通过深入源码来解释一些问题,比如经典面试题 “React 中的 setState 是同步的还是异步的?”。如果你精力有限的话,可以跳过本篇,毕竟工作不需要掌握源码,当你感兴趣了,再来看也不迟。

// ...

const [num, setNum] = React.useState(0)
const add = () => {
  setNum(num + 1)
  console.log(num) // 打印出来的是 num + 1 之前的值
  setNum(num + 2)
  console.log(num) // 打印出来的仍是 num + 1 之前的值
}

//...

在上面的代码中我们发现,每次调用add都会将 num + 2,并且 console.log 出来的都是上一次的值,我们通过解析 React 中的源码来理解这个问题。

一、首次渲染

温馨提示:当前debug 的 React 版本为 18.1.0。 最好你能有链表的知识,会更容易理解~

下面这是一段在React首次渲染时调用你写的 const [num, setNum] = useState(0)useState时最终会调用的函数,建议复制到编辑器中查看,或者在 codesandbox 中打开查看 效果更佳。

function mountState(initialState) {
  const hook = mountWorkInProgressHook();
  if (typeof initialState === 'function') {
    initialState = initialState();
  }
  
  hook.memoizedState = hook.baseState = initialState;
  // 注意这里 queue 是挂在hook上的,(hook 其实是跟 App 组件关联着的)
  const queue = (hook.queue = {
    pending: null,
    interleaved: null,
    lanes: NoLanes,
    dispatch: null,
    lastRenderedReducer: basicStateReducer,
    lastRenderedState: initialState,
  });
  
  const dispatch = (queue.dispatch = (dispatchAction.bind(
    null,
    currentlyRenderingFiber, // App 这个组件 fiber
    queue,
  )));
  // **** 3 ****
  return [hook.memoizedState, dispatch];
}

因为我们是首次执行,所以会进入到mountState内,我们只需要看下面这三个地方:

  1. line7 这里将initialState赋值给hook.memoizedState

  2. line18,创建一个dispatch函数,本质是dispatchAction,这里事先会传两个参数,你就当作为了定位将来是什么地方触发了这个函数即可

  3. line24,返回一个数组,内容也就是我们传入的initialState与一个dispatch

二、点击触发事件

在点击button去更新时(也就是去更新num时),我下面只展示部分重要的代码 (代码太长,并且掘金的代码展示不够友好,建议复制到编辑器中查看,或者在 codesandbox 中打开查看效果更佳)

function dispatchSetState(fiber, queue, action) {

  const update = {
    lane: lane,
    action: action, // 新的state
    eagerReducer: null,
    eagerState: null,
    next: null
  };
  
  const _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;
  
  var alternate = fiber.alternate;

  // 因为是当前fiber的第一个更新,所以会进入这个条件判断,直接看 line35
  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;

      try {
        var currentState = queue.lastRenderedState;
        // lastRenderedReducer 内判断 action 是 function,就会执行 action(currentState),返回新的state
        // action 也就是 setNum(n => n + 1) 中的函数
        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.
          return;
        }
      } catch (error) {// Suppress the error. It will throw again in the render phase.
      } finally {
        {
          ReactCurrentDispatcher$1.current = prevDispatcher;
        }
      }
    }
  }
  
  var root = scheduleUpdateOnFiber(fiber, lane, eventTime);
  
  // ......
}

其中fiberqueue就是之前的dispatchSetState中先传过来的两个值,action就是本次更新进来的新数据。

  1. 第一个setNum(本质上是dispatchSetState)接收新的值(1),也就是dispatchSetState中的action

  2. line 13 此时的 pendingnull(记得mountState中设置的吗?),所以update.next = update(设置成一个环形链表)

  3. line21,将新的值放到queue上(queue属于fiber对象上)

  4. 因为是第一个更新,所以 line25 条件判断成功,会在 line44 将update.hasEagerState = true; update.eagerState = eagerState(更新后的值);(注意这里,后面会使用到)

  5. 下面会调用scheduleUpdateOnFiber,在这里会标记需要 render

  6. 开始调用第二个setNum,跟上面的步骤是一样的,其中第 2 点和第 4 点不同

    a. line13 此时的pending是存在的 ,将这次的更新放到链表,那么两次更新就都到了queue.pending中了

    b. line26 当前 fiber 是已经存在了lane,所以不会进入到这里。

image.png

  1. add执行完以后,React 会根据标记进行 render 重新执行 App 这个函数(请你不要认为onClick就是执行完我们写的add就完事了,onClick是被 React 代理的,执行完我们的add以后,React 还会针对事件做处理的)

三、再次渲染

  1. 再次来到const [num, setNum] = React.useState(0)这里,执行这个useState
useState: function (initialState) {
  currentHookNameInDev = 'useState';
  updateHookTypesDev();
  var prevDispatcher = ReactCurrentDispatcher$1.current;
  ReactCurrentDispatcher$1.current = InvalidNestedHooksDispatcherOnUpdateInDEV;

  try {
    return updateState(initialState);
  } finally {
    ReactCurrentDispatcher$1.current = prevDispatcher;
  }
},

我们可以看到,这里执行并返回updateState(initialState),实际上,updateState最终执行的就是下面的这个updateReducer,如果你对这一大段代码不感兴趣,或看下面的解析仍看不明白,你看最后的函数返回也行。(同样建议复制到编辑器中查看,或在 codesandbox 中查看)

function updateReducer(reducer, initialArg, init) {
  var hook = updateWorkInProgressHook(); // 根据当前的 fiber(App)拿到当前的这个 useState
  var queue = hook.queue; // queue 就是上面截图的那个浏览器调试中的 queue
  
  queue.lastRenderedReducer = reducer;
  var current = currentHook; // The last rebase update that is NOT part of the base state.
  
  var baseQueue = current.baseQueue; // The last pending update that hasn't been processed yet.
  // pendingQueue 就是等待更新的 state 信息
  var pendingQueue = queue.pending;
  
  if (pendingQueue !== null) { // 更新队列不为空,也就意味着有更新的需要
    // We have new updates that haven't been processed yet.
    // We'll add them to the base queue.
    if (baseQueue !== null) { // baseQueue 此处等于 null
      // Merge the pending queue and the base queue.
      var baseFirst = baseQueue.next;
      var pendingFirst = pendingQueue.next;
      baseQueue.next = pendingFirst;
      pendingQueue.next = baseFirst;
    }

    current.baseQueue = baseQueue = pendingQueue; // 将 baseQueue 更新
    queue.pending = null; // 清空更新队列
  }
  
  if (baseQueue !== null) { // 此时 baseQueue 已经是不为空的了
    // We have a queue to process.
    // baseQueue 是最新的一个更新,也就是 setNum(n => n + 2) 的那个
    // 又因为这是一个环形链表,所以它的 next 也就是最早的那个 setNum(n => n + 1)
    // 所以这里 first 就是最早的那个更新
    var first = baseQueue.next;
    // baseState是更新之前的值,如果是第一个onclick的话也就是 0
    var newState = current.baseState;
    var newBaseState = null;
    var newBaseQueueFirst = null;
    var newBaseQueueLast = null;
    var update = first;

    do {
      var updateLane = update.lane;

      // This update does have sufficient priority.
      // 优先级很高的情况会被放到前面,不考虑这里
      if (newBaseQueueLast !== null) {
        var _clone = {
          // 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,
          action: update.action,
          hasEagerState: update.hasEagerState,
          eagerState: update.eagerState,
          next: null
        };
        newBaseQueueLast = newBaseQueueLast.next = _clone;
      } // Process this update.

      // 第一个更新时 hasEagerState 是已经被设置成了 true的
      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; // 直接设置成新的 state
      } else {
        // 后续的重复 setState 都走这里
        var action = update.action;
        // action(setState的形参)如果是一个function时,reducer 里面会执行 action(newState) 否则直接返回 action 
        // 所以 setState 里面传 function 时,里面的function通过参数能拿到的 state 都是前面 setState 的值
        newState = reducer(newState, action);
      }

      update = update.next; // 下一个更新
    } while (update !== null && update !== first); // 从第一个setState跑到最后一个setState

    if (newBaseQueueLast === null) {
      newBaseState = newState; // 设置最新的 State
    } else {
      newBaseQueueLast.next = newBaseQueueFirst;
    } // Mark that the fiber performed work, but only if the new state is
    // different from the current state.


    if (!objectIs(newState, hook.memoizedState)) {
      markWorkInProgressReceivedUpdate(); // 标记为需要更新
    }

    hook.memoizedState = newState; // 新的 state
    hook.baseState = newBaseState;
    hook.baseQueue = newBaseQueueLast; // 更新队列为 null 了
    queue.lastRenderedState = newState;
  }
  
  var dispatch = queue.dispatch;
  return [hook.memoizedState, dispatch]; // 最后返回新的 state 和 dispatch
}
  1. 根据当前的 fiber(App)拿到 hook(useState),判断pendingQueue是否为空,这里我们这个hook是需要更新的,进入判断

  2. 进入 line40 的 do while,直接看 line 60,第一个更新时 hasEagerState 是已经被设置成了 true的,在这里设置新的 state。后续循环时就走false了,逻辑一样,也是拿到本次 setState 的值

  3. 到 line72 将update设置成下一个setState的信息

  4. line 73 的while会判断如果update存在,并且update与上一个更新不相等的话会再进入一边循环(这里就是判断后面还是否有setState需要去处理)

  5. 最后一顿操作return [hook.memoizedState, dispatch],也就是这里会返回新的numsetNum供后面使用。

image.png

解答

所以,我们再回到问题 React 中的setState 是同步的还是异步的? 那么现在我们有了答案👇

其实,从本质上讲根本就没有什么同步异步一说,只是 React 会为了性能着想,将多次的 setState 放到一个更新队列里面,挂在这个 hook 上,在再次 useState 时(render时),通过 fiber 找到对应的 hook,如果这个 hook 上存在着这个更新队列,则将其合并成为一个,最后返回新的 state 和 dispatch。

其中,如果第一个 setState 中传入的是 function,会在第一次 setState 时就执行里面的函数,后续的 setState (与第一个传入的是不是function无关)会在再次 useState 时合并操作时依次调用 setState 中的 function,function 里面传入的 state 是当前 setState 之前的最新值。(可以看 line69)

这篇真的写的好幸苦啊,如果你觉得还不错的话,希望你能给我点个友好的赞👍 给予我鼓励,谢谢~

image.png