react原理:类组件的状态更新

1,122 阅读8分钟

点击这里进入react原理专栏

这篇文章会分别讲解一个类组件是如何创建和更新的,也就是类组件的初次挂载和后续更新的流程。

首先看更新类组件的入口:updateClassComponent

function updateClassComponent(current, workInProgress, Component, nextProps, renderLanes) {
  // 。。。
  if (instance === null) {
    // 挂载
    if (current !== null) {
      current.alternate = null;
      workInProgress.alternate = null;
      workInProgress.flags |= Placement;
    }
    // 创建类实例
    constructClassInstance(workInProgress, Component, nextProps);
    // 挂载类组件
    mountClassInstance(workInProgress, Component, nextProps, renderLanes);
    shouldUpdate = true;
  } else if (current === null) {
  	shouldUpdate = resumeMountClassInstance(workInProgress, Component, nextProps, renderLanes);
  } else {
    // 更新
    shouldUpdate = updateClassInstance(current, workInProgress, Component, nextProps, renderLanes);
  }
  // diff
  var nextUnitOfWork = finishClassComponent(current, workInProgress, Component, shouldUpdate, hasContext, renderLanes);
  // 。。。
  return nextUnitOfWork;
}

上面的注释已经把各个函数的调用阶段说的比较明白了,接下来先看挂载阶段的流程

finishClassComponent方法涉及到diff的流程,内容比较多,因此本篇暂时不讲。

挂载阶段

首先是创建类实例:constructClassInstance

function constructClassInstance(workInProgress, ctor, props) {
  // ...
  var instance = new ctor(props, context);
  var state = workInProgress.memoizedState = instance.state !== null && instance.state !== undefined ? instance.state : null;
  adoptClassInstance(workInProgress, instance);
  // ...
}

最主要的就是这三行代码,首先执行类的constructor方法,之后确定state,这里,fibermemoizedState就是本次更新结束之后,组件的state。由于时初次挂载,直接使用constructor中定义的this.state即可,接下来是adoptClassInstance

function adoptClassInstance(workInProgress, instance) {
  instance.updater = classComponentUpdater;
  workInProgress.stateNode = instance;
  // 指定一个内部属性
  set(instance, workInProgress);
}

这里为类实例添加了一个updater属性,看一下setState的源码

Component.prototype.setState = function (partialState, callback) {
  // ...
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

再看一下classComponentUpdater是什么

var classComponentUpdater = {
  isMounted: isMounted,
  enqueueSetState: function (inst, payload, callback) {
    // ...
  },
  enqueueReplaceState: function (inst, payload, callback) {
    // ...
  },
  enqueueForceUpdate: function (inst, callback) {
    // ...
  }
};

相信大家已经明白了。

类实例创建完成后,看一下挂载组件的mountClassInstance方法

function mountClassInstance(workInProgress, ctor, newProps, renderLanes) {
  // ...
  processUpdateQueue(workInProgress, newProps, instance, renderLanes);
  instance.state = workInProgress.memoizedState;
  var getDerivedStateFromProps = ctor.getDerivedStateFromProps;

  if (typeof getDerivedStateFromProps === 'function') {
    applyDerivedStateFromProps(workInProgress, ctor, getDerivedStateFromProps, newProps);
    instance.state = workInProgress.memoizedState;
  }

  if (typeof ctor.getDerivedStateFromProps !== 'function' && typeof instance.getSnapshotBeforeUpdate !== 'function' && (typeof instance.UNSAFE_componentWillMount === 'function' || typeof instance.componentWillMount === 'function')) {
    callComponentWillMount(workInProgress, instance); 
    processUpdateQueue(workInProgress, newProps, instance, renderLanes);
    instance.state = workInProgress.memoizedState;
  }
  // ...
}

这个方法主要做了两件事:计算state(processUpdateQueue)和生命周期方法的调用。关于state的计算,会在后面讲解。从生命周期部分的代码可以看到,react会先调用getDerivedStateFromProps钩子,之后如果没有新的生命周期(getDerivedStateFromPropsgetSnapshotBeforeUpdate),调用UNSAFE_componentWillMount。由于UNSAFE_componentWillMount中可能会再次更新state,所以会再次调用processUpdateQueue

后续更新

类组件更新的入口函数为updateClassInstance。但是在讲解该方法之前,先看一下类组件时如何产生更新的,也就是setState的流程。

前面已经看过了,setState会调用classComponentUpdaterenqueueSetState,定义如下

enqueueReplaceState: function (inst, payload, callback) {
  var fiber = get(inst);
  var eventTime = requestEventTime();
  var lane = requestUpdateLane(fiber);
  // 创建更新
  var update = createUpdate(eventTime, lane);
  update.tag = ReplaceState;
  // payload就是setState的第一个参数
  update.payload = payload;

  if (callback !== undefined && callback !== null) {
    {
      warnOnInvalidCallback(callback, 'replaceState');
    }
    // callback就是setState的回调
    update.callback = callback;
  }
  // 将update对象放入队列
  enqueueUpdate(fiber, update);
  // 调度更新
  scheduleUpdateOnFiber(fiber, lane, eventTime);
}

首先看一下fiber的一个属性:fiber.updateQueueupdateQueue中存放着fiber中的所有更新,updateQueue有一个shared属性,该属性是一个环形链表,shared有一个pending属性,该属性指向环形链表的最后一个节点,就是最新的一个update对象。知道了这些,enqueueUpdate的源码就能看懂了

// enqueueUpdate就是讲update放入环形链表中,并让pending执行最新的一个update
function enqueueUpdate(fiber, update) {
  var updateQueue = fiber.updateQueue;

  if (updateQueue === null) {
    return;
  }

  var sharedQueue = updateQueue.shared;
  var pending = sharedQueue.pending;

  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }

  sharedQueue.pending = update;
}

之后,我们又来到了scheduleUpdateOnFiber方法。看这段代码

if (
(executionContext & LegacyUnbatchedContext) !== NoContext &&
(executionContext & (RenderContext | CommitContext)) === NoContext) {
  schedulePendingInteractions(root, lane);
  performSyncWorkOnRoot(root);
} else {
  ensureRootIsScheduled(root, eventTime);
  schedulePendingInteractions(root, lane);
  
  if (executionContext === NoContext) {
    resetRenderTimer();
    flushSyncCallbackQueue();
  }
}

ReactDom.render这篇文章中提到过,只有在ReactDom.render才会进入if的逻辑,之后的更新会进入else的逻辑,因此,会执行ensureRootIsScheduled

ensureRootIsScheduled中会调度执行performSyncWorkOnRoot

function ensureRootIsScheduled(root, currentTime) {
  // ...
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else {
    // ...
  }
}

回想一下ReactDOM.render是直接执行performSyncWorkOnRoot,而setState则是调度执行performSyncWorkOnRootscheduleSyncCallback代码如下:

function scheduleSyncCallback(callback) {
  if (syncQueue === null) {
    syncQueue = [callback];
    immediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority, flushSyncCallbackQueueImpl);
  } else {
    syncQueue.push(callback);
  }

  return fakeCallbackNode;
}

Scheduler_scheduleCallbackscheduler模块暴露出的scheduleCallback,就是会在浏览器空闲时间调度某个函数。而scheduleSyncCallback就是将performSyncWorkOnRoot放入syncQueue中,之后调度flushSyncCallbackQueueImpl。但是在我们目前使用的legacy模式中,情况并不是这样的。

setState为什么是异步的

其实在类组件中,我们使用setState一般有两种情况,在生命周期中使用(componentDidMount),在点击事件中使用。在讲解ReactDOM.render和合成事件的文章中,我们忽略了两个方法:unbatchedUpdatesbatchedEventUpdates$1

function unbatchedUpdates(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext &= ~BatchedContext;
  executionContext |= LegacyUnbatchedContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

function batchedEventUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= EventContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

可以发现,这两个函数很像,都是执行回调函数fn之后,如果当前没有正在执行的任务(executionContext === NoContext),执行flushSyncCallbackQueue来清空回调队列(syncQueue)。在unbatchedUpdates中,fn就是updateContainer,在batchedEventUpdates$1中,fn就是dispatchEventsForPlugins。再结合ensureRootIsScheduled中调用的scheduleSyncCallback,就能直到为什么setState是异步的了。因为在react中,会将setState触发的更新函数performSyncWorkOnRoot放入一个队列中(syncQueue),等同步代码执行完毕后,再来执行回调队列中的任务。

现在大家可能又疑问了,scheduleSyncCallback不是也调度了一个任务吗,难道说performSyncWorkOnRoot会执行两次吗。当然不会,看一下flushSyncCallbackQueue的代码

function flushSyncCallbackQueue() {
  if (immediateQueueCallbackNode !== null) {
    var node = immediateQueueCallbackNode;
    immediateQueueCallbackNode = null;
    Scheduler_cancelCallback(node);
  }

  flushSyncCallbackQueueImpl();
}

Scheduler_scheduleCallback调度任务一个任务对象,赋值给immediateQueueCallbackNode,在执行flushSyncCallbackQueue时又取消了该任务(flushSyncCallbackQueueImpl),之后再执行flushSyncCallbackQueueImpl,开始执行syncQueen中的任务。至于为什么要这么做,个人认为,react17作为一个过渡版本,为了渐进升级而做的一种兼容。

因为scheduler中使用宏任务来调度任务,而unbatchedUpdatesbatchedEventUpdates$1中的代码是同步代码,因此会先于scheduler执行

至于flushSyncCallbackQueueImpl的内容就比较简单了,就是循环syncQueue中的任务

function flushSyncCallbackQueueImpl() {
  if (!isFlushingSyncQueue && syncQueue !== null) {
    // 一个开关,防止重复循环任务队列
    isFlushingSyncQueue = true;
    var i = 0;

    {
      try {
        var _isSync2 = true;
        var _queue = syncQueue;
        runWithPriority$1(ImmediatePriority$1, function () {
          for (; i < _queue.length; i++) {
            var callback = _queue[i];
            do {
              callback = callback(_isSync2);
            } while (callback !== null);
          }
        });
        syncQueue = null;
      } catch (error) {
         // ...
      }
    }
  }
}

setState有时还是同步的?

如果我们在setState外面加上了一层setTimeoutsetState就又变成了同步执行,这又是为什么呢?其实,setState异步执行不仅仅因为reactperformSyncWorkOnRoot放入了一个队列中,还有一个重要原因就是执行时机。注意scheduleUpdateOnFiber中有这样一段代码

if (
  // ...
} else {
  ensureRootIsScheduled(root, eventTime);
  schedulePendingInteractions(root, lane);

  if (executionContext === NoContext) {
    resetRenderTimer();
    flushSyncCallbackQueue();
  }
}

setState会执行ensureRootIsScheduled,将performSyncWorkOnRoot放入队列中,之后有一个判断,如果executionContext === NoContext,就执行flushSyncCallbackQueue。由于我们的setState是在setTimeout中的,当执行setTimeout的回调函数时,是获取不到react内部的执行上下文的,因此executionContext === NoContext成立,执行flushSyncCallbackQueue,之后就会取消ensureRootIsScheduled调度的任务,执行任务队列中的performSyncWorkOnRoot,这就是setState同步执行的原因。

针对这个问题,react提供了一个api:unstable_batchedUpdates,代码也很简单,就是额外提供了一个执行上下文:

function batchedUpdates$1(fn, a) {
  var prevExecutionContext = executionContext;
  executionContext |= BatchedContext;

  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;

    if (executionContext === NoContext) {
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
}

// 这样使用即可
unstable_batchedUpdates(
  setTimeout(() => {
    setState({
      // ...
    })
  })
)

setState批处理的原理

此外,react还提供了批处理的功能。其实,前面关于setState异步原因的讨论中,已经涉及到批处理的内容了,这里主要讲一下,当我们使用多个setState时,react是如何保证只触发一次更新的,重点在于ensureRootIsScheduled

function ensureRootIsScheduled(root, currentTime) {
   var existingCallbackNode = root.callbackNode;
   // ...
   if (existingCallbackNode !== null) {
    var existingCallbackPriority = root.callbackPriority;

    if (existingCallbackPriority === newCallbackPriority) {
      // 后续setState直接返回
      return;
    } 
    cancelCallback(existingCallbackNode);
   } 
    // ...
   if (newCallbackPriority === SyncLanePriority) {
     newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
   } else {
     // ...
   }
   // ...
   root.callbackNode = newCallbackNode;
}

当第一次进入ensureRootIsScheduledroot.callbackNode为空,之后经过scheduleSyncCallbackroot.callbackNode被赋值为newCallbackNode。之后的setState还会进入ensureRootIsScheduled,而此时existingCallbackNode不为空,并且多个setState的优先级相同,因此会直接返回。也就是说,只有第一次setState会将更新入口函数performSyncWorkOnRoot放入队列中,后续setState只会将update对象放入fiber.updateQueue中,不会触发更新入口函数。

如果在setTimeout中使用多次setState,因为没有了执行上下文,在scheduleUpdateOnFiber中,ensureRootIsScheduled调度任务后,就会执行flushSyncCallbackQueue

类组件的更新入口

讲了这么多,都在说setState的原理,现在回到类组件的更新部分,入口函数为updateClassInstance。这部分内容比较简单,首先就是执行componentWillReceiveProps生命周期(如果没有新的生命周期钩子的话),之后计算更新。

state的计算原理

终于到了这篇文章的重头戏:state是如何计算的。入口函数为processUpdateQueue。这部分代码很长,所以我们先明白整体流程,再看源码。

processUpdateQueue方法是兼容concurrent mode的,所以我们考虑这个例子:

A1 -> B2 -> C1 -> D1

字母代表state的变化,数字代表优先级,数字越小,优先级越高。

因此在concurrent mode下,B2由于优先级较低,会被跳过,因此第一次更新计算过程中,结果为ACD,下一轮更新计算时,就计算B了。并且我们要保证state的最终结果是正确的,也就是ABCD,不能是ACDB。因此,react在处理更新时,使用了第二条链表:baseUpdate链表以及另一个属性:baseState。下面来梳理一下整个流程

  1. 首先,因为updateQueue是环形链表,因此要将它剪断,成为一个单向链表
  2. 遍历updateQueue,来到A1,因为优先级足够,因此对A1进行状态计算
  3. 来到B2,由于优先级不够,将B2放到baseUpdate链表中,并且B2之后的所有节点都放入baseUpdate中,将A1作为baseStatebaseUpdate变为B2 -> C1 -> D1
  4. 之后的C1和D1的优先级都足够,进行计算
  5. 此时页面显示ACD
  6. 第二轮更新开始,此时updateQueue中没有内容(updateQueue存放的是用户产生的新更新,不是上次遗留的更新。如果updateQueue中有更新的话,将updateQueue剪断,连接到baseUpdate后面)
  7. 遍历baseUpdate链表,以baseState作为基准,开始计算,也就是说,从B2开始,以A1为基准,计算state。此时的baseUpdateB2 -> C1 -> D1
  8. 计算完毕,页面显示ABCD

因此状态更新计算的关键内容在于,baseUpdate保存因为优先级不足而被跳过的节点(包括该节点后面的所有节点),baseState保存被跳过节点的前一个节点计算之后的state,下一轮的更新就会基于这个baseState,将baseUpdate再循环一遍。

源码

function processUpdateQueue(workInProgress, props, instance, renderLanes) {
  var queue = workInProgress.updateQueue;
  hasForceUpdate = false;

  {
    currentlyProcessingQueue = queue.shared;
  }
  
  // react使用firstBaseUpdate表示baseUpdate链表的第一个节点,lastBaseUpdate表示最后一个节点
  var firstBaseUpdate = queue.firstBaseUpdate;
  var lastBaseUpdate = queue.lastBaseUpdate;
  var pendingQueue = queue.shared.pending;

  if (pendingQueue !== null) {
    // 剪断环形链表
    queue.shared.pending = null; 

    var lastPendingUpdate = pendingQueue;
    var firstPendingUpdate = lastPendingUpdate.next;
    lastPendingUpdate.next = null;
    // 将updateQueue拼接到baseUpdate后面
    if (lastBaseUpdate === null) {
      firstBaseUpdate = firstPendingUpdate;
    } else {
      lastBaseUpdate.next = firstPendingUpdate;
    }

    lastBaseUpdate = lastPendingUpdate;

    var current = workInProgress.alternate;
    // 对current做同样的操作,是为了保证即便本次更新被打断,之后根据current创建workInProgress时,能够得到正确的更新队列
    if (current !== null) {
      var currentQueue = current.updateQueue;
      var currentLastBaseUpdate = currentQueue.lastBaseUpdate;

      if (currentLastBaseUpdate !== lastBaseUpdate) {
        if (currentLastBaseUpdate === null) {
          currentQueue.firstBaseUpdate = firstPendingUpdate;
        } else {
          currentLastBaseUpdate.next = firstPendingUpdate;
        }

        currentQueue.lastBaseUpdate = lastPendingUpdate;
      }
    }
  }


  if (firstBaseUpdate !== null) {
    var newState = queue.baseState;

    var newLanes = NoLanes;
    var newBaseState = null;
    var newFirstBaseUpdate = null;
    var newLastBaseUpdate = null;
    var update = firstBaseUpdate;

    do {
      var updateLane = update.lane;
      var updateEventTime = update.eventTime;

      if (!isSubsetOfLanes(renderLanes, updateLane)) {
        // 该节点优先级不足
        var clone = {
          eventTime: updateEventTime,
          lane: updateLane,
          tag: update.tag,
          payload: update.payload,
          callback: update.callback,
          next: null
        };
        // 把被跳过的节点放入baseUpdate链表中
        if (newLastBaseUpdate === null) {
          newFirstBaseUpdate = newLastBaseUpdate = clone;
          newBaseState = newState;
        } else {
          newLastBaseUpdate = newLastBaseUpdate.next = clone;
        }


        newLanes = mergeLanes(newLanes, updateLane);
      } else {
        // 该节点优先级足够
        if (newLastBaseUpdate !== null) {
          // 如果之前有过优先级不足的节点,则该优先级不足节点之后的每个节点,都要被放入baseUpdate链表中
          var _clone = {
            eventTime: updateEventTime,
            lane: NoLane,
            tag: update.tag,
            payload: update.payload,
            callback: update.callback,
            next: null
          };
          newLastBaseUpdate = newLastBaseUpdate.next = _clone;
        }
        // 计算新的state
        newState = getStateFromUpdate(workInProgress, queue, update, newState, props, instance);
        var callback = update.callback;
	// setState的回调函数,放入effects数组中
        if (callback !== null) {
          workInProgress.flags |= Callback;
          var effects = queue.effects;

          if (effects === null) {
            queue.effects = [update];
          } else {
            effects.push(update);
          }
        }
      }
      // 循环链表
      update = update.next;

      if (update === null) {
        pendingQueue = queue.shared.pending;

        if (pendingQueue === null) {
          break;
        } else {
          // 如果有了新的update产生,放入updateQueue
          var _lastPendingUpdate = pendingQueue;
          var _firstPendingUpdate = _lastPendingUpdate.next;
          _lastPendingUpdate.next = null;
          update = _firstPendingUpdate;
          queue.lastBaseUpdate = _lastPendingUpdate;
          queue.shared.pending = null;
        }
      }
    } while (true);

    if (newLastBaseUpdate === null) {
      newBaseState = newState;
    }

    queue.baseState = newBaseState;
    queue.firstBaseUpdate = newFirstBaseUpdate;
    queue.lastBaseUpdate = newLastBaseUpdate;
    markSkippedUpdateLanes(newLanes);
    workInProgress.lanes = newLanes;
    // memoizedState就是本次状态更新计算完成之后的state
    workInProgress.memoizedState = newState;
  }

  {
    currentlyProcessingQueue = null;
  }
}

在计算state时,使用Object.assign进行合并

总结

本文讲解了setState的流程,类组件的状态更新原理,以及setState异步的原因,希望能够帮到大家。