setState 的场景及解读

1,197 阅读9分钟

用过 react 的小伙伴一定知道 setState 这个API,今天我们来看下 setState 用在不同场景中有什么不同。

注意,这里基于react 16.13.0

合成事件中的setState

首先得了解一下什么是合成事件,react为了解决跨平台,兼容性问题,自己封装了一套事件机制,代理了原生的事件,像在jsx中常见的onClick、onChange这些都是合成事件。

class App extends Component {
  state = { count: 0 }
  increase = () => {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count) // 输出的是更新前的count --> 0
  }
  render() {
    return (
      <div onClick={this.increase}>
        count: {this.state.count}
      </div>
    )
  }
}

生命周期函数中的setState

class App extends Component {
  state = { count: 0 }
  componentDidMount() {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count) // 输出的是更新前的count --> 0
  }
  render() {
    return (
      <div onClick={this.increase}>
        count: {this.state.count}
      </div>
    )
  }
}

在原生事件上的setState

class App extends Component {
  state = { count: 0 }
  componentDidMount() {
    document.body.addEventListener('click', this.changeCount, false)
  }
  changeCount = () => {
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count); // 输出的是更新后的count --> 1
  }
  render() {
    return (
      <div>
        count: {this.state.count}
      </div>
    )
  }
}

setTimeout 中的setState

class App extends Component {
  state = { count: 0 }
  componentDidMount() {
    setTimeout(_ => {
      this.setState({ count: this.state.count + 1 })
      console.log(this.state.count) // 输出的是更新后的count --> 1
    }, 0)
  }
  render() {
    return (
      <div>
        count: {this.state.count}
      </div>
    )
  }
}

批量 setState 更新

class App extends Component {
  state = { count: 0 }
  increase = () => {
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    this.setState({ count: this.state.count + 1 });
    console.log(this.state.count) // 输出的是更新前的count --> 0
  }
  render() {
    return (
      <div onClick={this.increase}>
        count: {this.state.count} // 0
      </div>
    )
  }
}

综上,出个题目考考大家。如果大家读了上面的内容应该很快能够给出答案。

class App extends React.Component {
  state = { count: 0 }
  componentDidMount() {
    this.setState({ count: this.state.count + 1 })
    console.log(this.state.count);

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

    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);
    }, 0)
  }

  render() {
    return <div>{this.state.count}</div> // 3
  }
}

小结

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。

  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。

  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

那 setState 在不同场景下为什么会有不一样的表现呢?接下来我们来看下原因

通过源码解读:

首先 Component 定义props、context、refs,之后注入一个更新器。

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  this.refs = emptyObject; // 如果一个组件有字符串引用,我们稍后将分配一个不同的对象。
  this.updater = updater || ReactNoopUpdateQueue; // 初始化更新器,实际没有则使用默认更新器
}

我们可以看到 setState 是在组件的原型上定义的,它有两个参数:partialState 和 callback。首先它会检测 partialState 的类型,partialState 是一个object、function或者null,如果不是这三个类型就会抛错。然后执行更新器的setState队列。执行 enqueueSetState。

Component.prototype.setState = function (partialState, callback) {
  if (!(typeof partialState === 'object' || typeof partialState === 'function' || partialState == null)) {
    { ... // 提示 }
  }
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

在更新器上的检测操作,检测如果在组件没有 mounted,就调用 setState 会报警告。

var ReactNoopUpdateQueue = {
  ...
  enqueueSetState: function (publicInstance, partialState, callback, callerName) {
    warnNoop(publicInstance, 'setState');
  }
};
function warnNoop(publicInstance, callerName) {
  {
    var _constructor = publicInstance.constructor;
    var componentName = _constructor && (_constructor.displayName || _constructor.name) || 'ReactClass';
    var warningKey = componentName + "." + callerName;

    if (didWarnStateUpdateForUnmountedComponent[warningKey]) {
      return;
    }
    error("错误提示", callerName, componentName);
    didWarnStateUpdateForUnmountedComponent[warningKey] = true;
  }
}

接下来,在 DOM 加载的时候把 updater 注入进来。

var classComponentUpdater = {
  isMounted: isMounted, // 检测组件是否已经 mounted。
  
  // setState队列
  enqueueSetState: function (inst, payload, callback) {
    var fiber = get(inst);
    var currentTime = requestCurrentTimeForUpdate();
    var suspenseConfig = requestCurrentSuspenseConfig();
    var expirationTime = computeExpirationForFiber(currentTime, fiber, suspenseConfig);
    var update = createUpdate(expirationTime, suspenseConfig);
    update.payload = payload;

    if (callback !== undefined && callback !== null) {
      { warnOnInvalidCallback(callback, 'setState') }
      update.callback = callback;
    }

    enqueueUpdate(fiber, update);
    scheduleWork(fiber, expirationTime);
  },
  ...
};

我们直接来看 setState 队列,这里需要3个参数分别是实例对象,负载和回调函数。在这里我们先看在最开始声明5个变量分别是干什么用的。

  • fiber:通过对象记录组件上需要做或者已经完成的更新,一个组件可以对应多个Fiber。简单点来说,就是通过对象的形式描述一个DOM
function get(key) {
  return key._reactInternalFiber;
}
function has(key) {
  return key._reactInternalFiber !== undefined;
}
function set(key, value) {
  key._reactInternalFiber = value;
}

在render函数中创建的React Element树在第一次渲染的时候会创建一颗结构一模一样的Fiber节点树。不同的React Element类型对应不同的Fiber节点类型。一个React Element的工作就由它对应的Fiber节点来负责。

一个React Element可以对应不止一个Fiber,因为Fiber在更新的时候,会从原来的Fiber(current)克隆出一个新的Fiber(alternate)。两个Fiber diff出的变化(side effect)记录在alternate上。所以一个组件在更新时最多会有两个Fiber与其对应,在更新结束后alternate会取代之前的current的成为新的current节点。

  • currentTime:获取当前更新时间,是否批量跟新事件的不同时间处理
function requestCurrentTimeForUpdate() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // 我们在React内部,因此可以读取实际时间。设置同步更新的时间
    return msToExpirationTime(now());
  }
  // 可能处于浏览器事件的中间。这里是设置所谓的‘异步’更新的时间
  if (currentEventTime !== NoWork) {
    // 对所有更新使用相同的开始时间,直到我们再次输入React。
    return currentEventTime;
  } 
  // 这是自React产生以来的第一次更新。 计算新的开始时间。
  currentEventTime = msToExpirationTime(now());
  return currentEventTime;
}
  • suspenseConfig: 一些配置,不过多解释,有需要可以自己去查阅相关文档

我们一步步找上去,suspenseConfig 的逻辑如下:

var Internals = {
  Events: [
    getInstanceFromNode$1,
    getNodeFromInstance$1,
    getFiberCurrentPropsFromNode$1,
    injectEventPluginsByName,
    eventNameDispatchConfigs,
    accumulateTwoPhaseDispatches,
    accumulateDirectDispatches,
    enqueueStateRestore,
    restoreStateIfNeeded,
    dispatchEvent, // 这里有处理 setState 更新机制的逻辑,在文章最后介绍
    runEventsInBatch,
    flushPassiveEffects,
    IsThisRendererActing
  ]
};

exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = Internals

var ReactSharedInternals = React.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED;

var ReactCurrentBatchConfig = ReactSharedInternals.ReactCurrentBatchConfig;

function requestCurrentSuspenseConfig() {
  return ReactCurrentBatchConfig.suspense;
}
  • expirationTime
// 计算优先等级函数
function getCurrentPriorityLevel() {
  switch (Scheduler_getCurrentPriorityLevel()) {
    case Scheduler_ImmediatePriority:
      return ImmediatePriority; // 99
    case Scheduler_UserBlockingPriority:
      return UserBlockingPriority$1; // 98
    case Scheduler_NormalPriority:
      return NormalPriority; // 97
    case Scheduler_LowPriority:
      return LowPriority; // 96
    case Scheduler_IdlePriority:
      return IdlePriority; // 95
    default:
      {
        {
          throw Error( "Unknown priority level." );
        }
      }

  }
}
function computeExpirationForFiber(currentTime, fiber, suspenseConfig) {
  var mode = fiber.mode;
  // 同步模式,变量 noMode = 0
  if ((mode & BlockingMode) === NoMode) {
    return Sync;
  }
  
  var priorityLevel = getCurrentPriorityLevel(); // 计算优先等级
  
  // 并发模式
  if ((mode & ConcurrentMode) === NoMode) {
    return priorityLevel === ImmediatePriority ? Sync : Batched;
  }
 
  // 同步跟新的情况
  if ((executionContext & RenderContext) !== NoContext) {
    // 在 React 内部,同步更新
    return renderExpirationTime$1;
  }

  var expirationTime;

  if (suspenseConfig !== null) {
    // 根据挂起超时计算到期时间。
    // 如果timeoutMs小于正常pri到期时间,我们是否应该发出警告?
    expirationTime = computeSuspenseExpiration(currentTime, suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION);
  } else {
    // 根据调度程序优先级计算到期时间。
    switch (priorityLevel) {
      case ImmediatePriority:
        expirationTime = Sync;
        break;
      case UserBlockingPriority$1:
        expirationTime = computeInteractiveExpiration(currentTime);
        break;
      case NormalPriority:
      case LowPriority:
        expirationTime = computeAsyncExpiration(currentTime);
        break;
      case IdlePriority:
        expirationTime = Idle;
        break;
      default:
        {
          {
            throw Error( "Expected a valid priority level" );
          }
        }

    }
  }

  if (workInProgressRoot !== null && expirationTime === renderExpirationTime$1) {
    // 这是将此更新移到单独批处理中的技巧
    expirationTime -= 1;
  }

  return expirationTime;
}
  • update:更新器
function createUpdate(expirationTime, suspenseConfig) {
  var update = {
    expirationTime: expirationTime,
    suspenseConfig: suspenseConfig,
    tag: UpdateState,
    payload: null,
    callback: null,
    next: null
  };
  update.next = update;
  {
    update.priority = getCurrentPriorityLevel(); // 获取当前优先级
  }
  return update;
}

在定义了这五个变量之后,检测 callback 是否有效,有效则加入 update。接下来执行下面两个函数

  • enqueueUpdate(fiber, 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;

  {
    if (currentlyProcessingQueue === sharedQueue && !didWarnUpdateInsideUpdate) {
      ... // 提示

      didWarnUpdateInsideUpdate = true;
    }
  }
}
  • scheduleWork(fiber, expirationTime):执行更新。重点,这里就是 setState 的同步和‘异步’区分了
var scheduleWork = scheduleUpdateOnFiber; // 这被拆分为一个单独的函数,因此我们可以将 fiber 标记为待处理的工作(react的合成事件等),而无需将其视为源自事件的典型更新。

function scheduleUpdateOnFiber(fiber, expirationTime) {
  checkForNestedUpdates(); // React限制了嵌套更新的数量和最大更新深度。
  warnAboutRenderPhaseUpdatesInDEV(fiber); // DEV 中的警告
  var root = markUpdateTimeFromFiberToRoot(fiber, expirationTime); // 标记从 fiber 到根的更新时间
  
  if (root === null) {
    // 内存泄漏的警告
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return;
  }

  checkForInterruption(fiber, expirationTime); // 检查是否中断
  recordScheduleUpdate(); // 记录时间表更新

  var priorityLevel = getCurrentPriorityLevel(); // 获取当前优先级

  if (expirationTime === Sync) {
    if ( // 检查我们是否在批量更新内
    (executionContext & LegacyUnbatchedContext) !== NoContext && 
    (executionContext & (RenderContext | CommitContext)) === NoContext) { // 检查我们是否尚未渲染
      // 在根上注册待处理的交互,以避免丢失跟踪的交互数据。主要是更新记录异步函数的数量
      schedulePendingInteractions(root, expirationTime);
      // 根执行同步工作
      // 这是一个遗留的边缘情况。 在batchedUpdates内部的ReactDOM.render根的初始安装应该是同步的,但是布局更新应该推迟到批处理结束。
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root); // 确保根已安排更新处理
      schedulePendingInteractions(root, expirationTime);

      if (executionContext === NoContext) {
        // 立即清除同步工作,除非我们已经在工作或在批量处理中。故意将其放置在scheduleUpdateOnFiber而不是scheduleCallbackForFiber内,以保留在不立即刷新回调的情况下调度回调的功能。我们仅对用户启动的更新执行此操作,以保留旧版模式的历史行为。
        flushSyncCallbackQueue();
      }
    }
  } else {
    ensureRootIsScheduled(root);
    schedulePendingInteractions(root, expirationTime);
  }

  if ((executionContext & DiscreteEventContext) !== NoContext && (
  // 即使具有离散事件,也仅将具有用户阻塞优先级或更高优先级的更新视为离散。
  priorityLevel === UserBlockingPriority$1 || priorityLevel === ImmediatePriority)) {
    // 这是离散事件的结果。跟踪每个根目录的最低优先级离散更新,以便我们可以在需要时及早刷新它们。
    if (rootsWithPendingDiscreteUpdates === null) {
      rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
    } else {
      var lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);

      if (lastDiscreteTime === undefined || lastDiscreteTime > expirationTime) {
        rootsWithPendingDiscreteUpdates.set(root, expirationTime);
      }
    }
  }
}

这里是处理事件的机制:

function dispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
  if (!_enabled) {
    return;
  }

  // 如果我们已经有一个离散事件队列,并且这是另一个离散事件,则无论目标是什么,我们都无法分派它,因为它们需要按顺序分派。也就是说如果有排队的离散事件或是可重播的离散事件
  if (hasQueuedDiscreteEvents() && isReplayableDiscreteEvent(topLevelType)) {
    queueDiscreteEvent(null, topLevelType, eventSystemFlags, container, nativeEvent);
    
    // 据我们所知,实际上我们并未对任何内容进行阻止的标志。
    return;
  }
  
  // 尝试调度事件看有无原生事件
  var blockedOn = attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent);

  if (blockedOn === null) {
    // 我们成功调度了此事件。
    clearIfContinuousEvent(topLevelType, nativeEvent);
    return;
  }

  // 如果是可重播的离散事件
  if (isReplayableDiscreteEvent(topLevelType)) {
    // 目标可用时,将在以后重播此内容。
    queueDiscreteEvent(blockedOn, topLevelType, eventSystemFlags, container, nativeEvent);
    return;
  }

  //  如果是连续事件则排队
  if (queueIfContinuousEvent(blockedOn, topLevelType, eventSystemFlags, container, nativeEvent)) {
    return;
  } 

  // 我们只需要在不排队的情况下进行清除,因为排队是累积性的。
  clearIfContinuousEvent(topLevelType, nativeEvent); 
  
  // 这是不可重播的,因此在事件系统需要跟踪它的情况下,我们将调用它,但没有目标
  {
    dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, null);
  }
}
function attemptToDispatchEvent(topLevelType, eventSystemFlags, container, nativeEvent) {
  // 原生事件
  var nativeEventTarget = getEventTarget(nativeEvent);
  // 从节点获取最近的实例
  var targetInst = getClosestInstanceFromNode(nativeEventTarget);

  if (targetInst !== null) {
    // 获得最近安装的 fiber
    var nearestMounted = getNearestMountedFiber(targetInst);

    if (nearestMounted === null) {
      // 该树已被卸载。 没有目标就派遣。
      targetInst = null;
    } else {
      var tag = nearestMounted.tag;

      if (tag === SuspenseComponent) { // SuspenseComponent = 13
        // 从 Fiber 获取挂起实例
        var instance = getSuspenseInstanceFromFiber(nearestMounted);

        if (instance !== null) {
          // 将事件排队以便稍后重播。 中止调度,因为我们不希望此事件通过事件系统调度两次。如果这是队列中的第一个离散事件。 为该边界安排更高的优先级。
          return instance;
        } 
        
        // 这不应该发生,出了点问题,但是为了避免阻塞整个系统,请在没有目标的情况下调度事件。
        targetInst = null;
      } else if (tag === HostRoot) { // HostRoot = 3
        var root = nearestMounted.stateNode;

        if (root.hydrate) {
          // 如果在重播期间发生这种情况,则可能出了点问题,可能会阻塞整个系统
          return getContainerFromFiber(nearestMounted);
        }

        targetInst = null;
      } else if (nearestMounted !== targetInst) {
        // 如果在提交该组件的安装之前收到一个事件(例如:img onload),请暂时将其忽略(即,将其视为非响应树上的事件)。 我们也可以考虑对事件进行排队,然后在挂载后调度它们。
        targetInst = null;
      }
    }
  }

  {
    dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst);
  } 
  
  // 我们没有任何阻碍。
  return null;
}

为旧版事件系统调度事件

function dispatchEventForLegacyPluginEventSystem(topLevelType, eventSystemFlags, nativeEvent, targetInst) {
  var bookKeeping = getTopLevelCallbackBookKeeping(topLevelType, nativeEvent, targetInst, eventSystemFlags);

  try {
    // 在合成事件前调用 batchedEventUpdates,以设置 isBatchingEventUpdates 为 true or false
    batchedEventUpdates(
      handleTopLevel, // 这里的逻辑应该不难理解,在调用任何事件处理程序之前,先构建祖先数组
      bookKeeping
    ); 
  } finally {
    releaseTopLevelCallbackBookKeeping(bookKeeping); // 初始化为调用 setState 事件前的状态
  }
}

这里是 setState 同步或者‘异步’的表现:如果 isBatchingEventUpdates 为 true,则先执行 batchedEventUpdatesImpl 这个函数,最后执行 setState 的事件

function batchedEventUpdates(fn, a, b) {
  if (isBatchingEventUpdates) {
    // 如果我们当前在另一个批次中,则需要等到它完全完成后再恢复状态。
    return fn(a, b);
  }

  isBatchingEventUpdates = true; // 决定 setState 为同步或者 ‘异步’的标识

  try {
    return batchedEventUpdatesImpl(fn, a, b);
  } finally {
    isBatchingEventUpdates = false;
    finishEventHandler();
  }
}

以下逻辑就不再继续介绍了,有需要的小伙伴可以自行查阅文档

function finishEventHandler() {
  // 在这里,我们等待所有更新均已传播,这在使用层中的受控组件时很重要,然后,我们恢复任何受控组件的状态。
  var controlledComponentsHavePendingUpdates = needsStateRestore();

  if (controlledComponentsHavePendingUpdates) {
    // 如果触发了受控事件,则可能需要将DOM节点的状态恢复回受控值。 当React退出更新而不接触DOM时,这是必需的。
    flushDiscreteUpdatesImpl();
    restoreStateIfNeeded();
  }
}

好了,今天就说到这里吧,有不对的地方请各位小伙伴指出哦。一起成长。

总结

setState 在原生事件或者 setTimeout 和 setInterval 事件中表现为同步。

setState 在合成事件和生命周期中表现为异步: 首先在合成事件和生命周期前会执行一个名为 batchedEventUpdates 的函数来设置 isBatchingEventUpdates 的值为 false 还是 true。如果为 false,则直接修改 state 表现为同步,如果值为 true,则先执行setState事件,但是没有更新状态,执行完了之后才去更新状态,所以在这里表现为‘异步’。