react经典面试题之this.setState

3,287

一、前言

react是当下非常流行的前端框架,如果你是一个react开发者,当你出去面试时大概会有80%以上的概率会被问到setState相关问题。比如setState是同步还是异步,setState批量更新时如何实现的等等。
很多同学可能给出的答案是setState是异步更新,其实不然。那么真正的答案是什么呢?接下来从源码上分析setState,从根本上理解。

二、legacy模式下的setState

lagacy模式就是目前reactDom使用的默认模式,即reactDom.render创建的应用。它与concurrent模式的区别就在于更新的优先级,lagacy模式的优先级为syncLane,即同步优先级。而concurrent是根据不同场景下会有不同的优先级。这里为什么提到优先级呢?因为reactDom在不同优先级下处理setState的逻辑是不相同的。调用setState后最终会调用scheduleUpdateOnFiber这个函数去调度一次更新,其中有一段代码是这样的

if (lane === SyncLane) {
  if (
  // executionContext & LegacyUnbatchedContext !== NoContext判断是否不是批量更新
  (executionContext & LegacyUnbatchedContext) !== NoContext && // Check if we're not already rendering
  (executionContext & (RenderContext | CommitContext)) === NoContext) {
    schedulePendingInteractions(root, lane); 
    // 同步调度更新
    performSyncWorkOnRoot(root);
  } else {
	// 异步调度更新
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);

    if (executionContext === NoContext) {
      resetRenderTimer();
      flushSyncCallbackQueue();
    }
  }
} else {
	// ...
}

正如我们上面所说的,reactDom.render创建的应用,它的lane会是SyncLane,也就会进入这段逻辑,这个if里面又有一个if else,而这个if else最关键的就是executionContext这个变量是否包含批量更新,如果不是批量更新,将会同步调用performSyncWorkOnRoot去开始更新,否则会通过ensureRootIsScheduled异步调用依次更新。
那么executionContext是在哪里赋值的呢?以click点击事件(react事件原理)调用setState为例,它最终会调用

batchedEventUpdates(function () {
  return dispatchEventsForPlugins(domEventName, eventSystemFlags, nativeEvent, ancestorInst);
});

而这个batchedEventUpdates会调用batchedEventUpdates$1

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

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

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

可以看到batchedEventUpdates$1将executionContext按位或EventContext,而这个EventContext就不包含了LegacyUnbatchedContext,这个fn就是我们onClick对应的回到函数,这样就进入了ensureRootIsScheduled异步调度更新的逻辑,所以这种情况下的setState是异步的。难道这就是正确答案了吗?显然不是,看下面一个列子

setTimeout(() => {
  this.setState({...})
})

当我们用setTimeout去调用setState时,this.setState()的调用栈就不在batchedEventUpdates1中,也就拿不到batchedEventUpdates1中,也就拿不到batchedEventUpdates1调用栈中的executionContext,所以会进入performSyncWorkOnRoot同步调度更新,所以在setTimeout中调用setState后我们马上能拿到更新后的state。所以这种情况下的setState是同步的。
到了这里,你是否认为这已经是正确答案了呢?显然不是,还有concurrent模式。

三、concurrent模式下的setState

concurrent的lane不是SyncLane,那么它就会进入scheduleUpdateOnFiber的else逻辑中

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

我们可以发现,concurrent模式下的setState永远都是异步调度的,也就是说即使是在setTimeout中调用setState,它也是异步的。
看到这里,相比大家已经知道了”setState是同步还是异步“的答案了。

四、setState如何实现批量更新

面试过程中,往往会被问到这样一个问题,在个函数中多次setState会更新几次?比如下面这个例子

setCount() {
  this.setState({count: 1})
  this.setState({count: 2})
  this.setState({count: 3})
}

很多同学都知道答案,那就是一次。那么为什么是一次呢?其实是react针对这种情况做了批量更新操作,下面我们就来看看react如何实现批量更新。
首先,要实现批量更新那么就要求此次更新时异步的,我们从上面的内容中已经知道了异步更新调用的是ensureRootIsScheduled,我们会发现,每次调用setState都会去调用ensureRootIsScheduled一次,我们来看看ensureRootIsScheduled是如何处理

function ensureRootIsScheduled(root, currentTime) {
  var existingCallbackNode = root.callbackNode; 
  // ...
  // 获取当前更新的优先级
  var newCallbackPriority = returnNextLanesPriority();

  // ...
  if (existingCallbackNode !== null) {
  // root.callbackPriority为上次setState的优先级
    var existingCallbackPriority = root.callbackPriority;

    if (existingCallbackPriority === newCallbackPriority) {
      return;
    } 
    cancelCallback(existingCallbackNode);
  }


  var newCallbackNode;
  // scheduler开头的函数是 scheduler调度器相关的api,会用不同的优先级向调度器注册任务,等待异步调用,并返回这个任务
  if (newCallbackPriority === SyncLanePriority) {
    newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
  } else if (newCallbackPriority === SyncBatchedLanePriority) {
    newCallbackNode = scheduleCallback(ImmediatePriority$1, performSyncWorkOnRoot.bind(null, root));
  } else {
    var schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
    newCallbackNode = scheduleCallback(schedulerPriorityLevel, performConcurrentWorkOnRoot.bind(null, root));
  }

  root.callbackPriority = newCallbackPriority;
  root.callbackNode = newCallbackNode;
}

这段代码写的非常巧妙,下面我们分析一下

  1. 第一次setState,此时existingCallbackNode为null,而更新的优先级newCallbackPriority自然是SyncLanePriority,那么就会用scheduleSyncCallback调度一次更新
  2. 将scheduleSyncCallback返回的结果task赋值给root.callbackNode,将此次setState的优先级赋值给root.callbackPriority,root为fiber树的根节点
  3. 第二次setState,此时existingCallbackNode为上一次的setState的task,就会进入existingCallbackNode !== null的逻辑,我们发现如果进入existingCallbackPriority === newCallbackPriority的逻辑,直接return掉,而不会再去向调度器注册一次更新任务,这样就能达到了批量更新的目的,那么如何保证多次setState会是同一个优先级呢? 当我们在调用setState时,调用的其实就是enqueueSetState方法,其中会调用requestUpdateLane去获取一个此次更新的优先级,那么react肯定是在这个函数中处理优先级的,接下来我们看看这个函数
function requestUpdateLane(fiber) {
  var mode = fiber.mode;
  if ((mode & BlockingMode) === NoMode) {
    return SyncLane;
  } else if ((mode & ConcurrentMode) === NoMode) {
    return getCurrentPriorityLevel() === ImmediatePriority$1 ? SyncLane : SyncBatchedLane;
  } 
 // ...

  var schedulerPriority = getCurrentPriorityLevel(); 

  var lane;

  if (
  (executionContext & DiscreteEventContext) !== NoContext && schedulerPriority === UserBlockingPriority$2) {
    lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
  } else {
    var schedulerLanePriority = schedulerPriorityToLanePriority(schedulerPriority);

    lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  }

  return lane;
}

我们可以看到这里区分了lagacy,blocking,concurrent三种模式

lagacy模式

lagacy模式会直接返回SyncLane,这比较简单

if ((mode & BlockingMode) === NoMode) {
  return SyncLane;
}

blocking模式(是一个中间模式,作为迁移到concurrent模式的一个过渡)

blocking模式会根据getCurrentPriorityLevel的调用结果,返回SyncLane或者SyncBatchedLane,而getCurrentPriorityLevel会根据当前schedule的优先级获取一个react优先级,而此次是在同一个schedule调度中,所以获取到的react优先级将是同一个

if ((mode & ConcurrentMode) === NoMode) {
  return getCurrentPriorityLevel() === ImmediatePriority$1 ? SyncLane : SyncBatchedLane;
} 

concurrent模式

if (currentEventWipLanes === NoLanes) {
  currentEventWipLanes = workInProgressRootIncludedLanes;
}

var schedulerPriority = getCurrentPriorityLevel(); 

var lane;

if (
(executionContext & DiscreteEventContext) !== NoContext && schedulerPriority === UserBlockingPriority$2) {
  // ...
} else {
  var schedulerLanePriority = schedulerPriorityToLanePriority(schedulerPriority);

  lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
}

return lane;

可以看到最终lane是根绝schedulerLanePriority和currentEventWipLanes来获取的,所以只要保证第n次setState和第一次setState的这两个变量相同就能达到目的。

  1. schedulerLanePriority是根据当前scheduler的执行优先级返回一个react优先级,因为此次都是在一个执行环节中,所以schedulerLanePriority是相同的
  2. currentEventWipLanes,第一次setState时会把workInProgressRootIncludedLanes赋值给currentEventWipLanes,而这个workInProgressRootIncludedLanes其实就是上一次更新lane,在第二次setState时,因为有同一个上层调用栈,所以能获取到currentEventWipLanes,这样就保证了多次setState能获取到相同的lane 这样通过不同模式的不同的处理逻辑,多次setState都能获得一个相同的lane,也就符合了ensureRootIsScheduled中的existingCallbackPriority === newCallbackPriority,从而达到批量更新的目的。