React的setState详解

我们使用React开发的时候,需要修改state中的数据,而我们并不能通过直接修改state中的数据来 实现界面的更新。当然用过Vue的都知道,Vue有自己的响应式系统,能够知道哪些数据发生了改变并自动更新视图。

setState的基本使用

React需要通过setState这个API来进行视图的重新渲染。这个函数是挂载在Component的原型上供我们使用。

另外我们还要遵循一个原则就是永远都不要直接修改state中的属性值,通过setState来修改也是。**
**如:

//这样修改是不会触发组件的更新
this.state.counter += 1;
//正确的方式如下
this.setState({
  counter:this.state.counter+1
})

setState的更新

setState是异步更新的

我们通过setState修改数据之后是不能立马获取改变后的值的。

//counter初值为1
this.setState({
  counter:this.state.counter+1
})
console.log(this.state.counter);//1

关于为什么setState是异步更新的在官方的github上有解释 链接

我们每次调用setState来更新时,视图同步更新,但是多次调用就可能使得视图更新频繁,影响性能,所以设计为异步更新方式是为了批处理更新状态,将多次的更新合并成一次更新。

另外如果同步更新state而没有及时执行render,那么state中的数据就和props数据不同步。

当然我们也可以获取异步更新后的记结果,setState第二个参数可以传入一个回调函数,在这个回调函数中就可以获取异步更新后的结果。

this.setState({
  counter:this.state.counter+1
}, () => {
    console.log(this.state.counter)
})

setState是同步更新的

在某些情况下我们可以让setState同步更新。

把函数放在计数器中,设置时间为0那就是同步执行, 另外放置在原生的事件执行的回调函数也是同步执行的。

setTimeout(() => {
  //初始为1  
  this.setState({
    counter:this.state.counter+1
  })
  console.log(this.state.counter) //2
}, 0)

当然整体上执行还是异步执行。

所以我们这里可以做个总结:

  • 当我们在组件的生命周期或者React的合成事件中执行setState,其是异步执行的。
  • 当我们在计数器中执行或者原生的回调函数执行setState,其是同步执行的。

setState是如何执行的

找到定义方法的地方:

Component.prototype.setState = function(partialState, callback) {
    //下面主要是校验和警告
  invariant(
    typeof partialState === 'object' ||
      typeof partialState === 'function' ||
      partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
      'function which returns an object of state variables.',
  );
  //这里是关键,这个updater是每个类组件都拥有的方法
  this.updater.enqueueSetState(this, partialState, callback, 'setState');
};

点击进入updater:

function Component(props, context, updater) {
  this.props = props;
  this.context = context;
  // If a component has string refs, we will assign a different object later.
  this.refs = emptyObject;
  // We initialize the default updater but the real one gets injected by the
  // renderer.
  this.updater = updater || ReactNoopUpdateQueue;
}

找到如下对象:**
**image-20210924161530354

主要是调用了enqueueSetState这个方法,注意eventTimelane这两个常量,点进后面的执行函数,这个函数主要是获取setState的执行上下文,来判断执行方式---同步或者异步。

export function requestEventTime() {
  if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
    // We're inside React, so it's fine to read the actual time.
    return now();
  }
  // We're not inside React, so we may be in the middle of a browser event.
  if (currentEventTime !== NoTimestamp) {
    // Use the same start time for all updates until we enter React again.
    return currentEventTime;
  }
  // This is the first update since React yielded. Compute a new start time.
  currentEventTime = now();
  return currentEventTime;
}
export function requestUpdateLane(fiber: Fiber): Lane {
  // Special cases
  const mode = fiber.mode;
    //根据mode来判断同步还是批处理
  if ((mode & ConcurrentMode) === NoMode) {
    return (SyncLane: Lane);
  } else if (
    !deferRenderPhaseUpdateToNextBatch &&
    (executionContext & RenderContext) !== NoContext &&
    workInProgressRootRenderLanes !== NoLanes
  ) {
    // This is a render phase update. These are not officially supported. The
    // old behavior is to give this the same "thread" (lanes) as
    // whatever is currently rendering. So if you call `setState` on a component
    // that happens later in the same render, it will flush. Ideally, we want to
    // remove the special case and treat them as if they came from an
    // interleaved event. Regardless, this pattern is not officially supported.
    // This behavior is only a fallback. The flag only exists until we can roll
    // out the setState warning, since existing code might accidentally rely on
    // the current behavior.
    return pickArbitraryLane(workInProgressRootRenderLanes);
  }

  const isTransition = requestCurrentTransition() !== NoTransition;
  if (isTransition) {
    if (
      __DEV__ &&
      warnOnSubscriptionInsideStartTransition &&
      ReactCurrentBatchConfig._updatedFibers
    ) {
      ReactCurrentBatchConfig._updatedFibers.add(fiber);
    }
    // The algorithm for assigning an update to a lane should be stable for all
    // updates at the same priority within the same event. To do this, the
    // inputs to the algorithm must be the same.
    //
    // The trick we use is to cache the first of each of these inputs within an
    // event. Then reset the cached values once we can be sure the event is
    // over. Our heuristic for that is whenever we enter a concurrent work loop.
    if (currentEventTransitionLane === NoLane) {
      // All transitions within the same event are assigned the same lane.
      currentEventTransitionLane = claimNextTransitionLane();
    }
    return currentEventTransitionLane;
  }

  // Updates originating inside certain React methods, like flushSync, have
  // their priority set by tracking it with a context variable.
  //
  // The opaque type returned by the host config is internally a lane, so we can
  // use that directly.
  // TODO: Move this type conversion to the event priority module.
  const updateLane: Lane = (getCurrentUpdatePriority(): any);
  if (updateLane !== NoLane) {
    return updateLane;
  }

  // This update originated outside React. Ask the host environment for an
  // appropriate priority, based on the type of event.
  //
  // The opaque type returned by the host config is internally a lane, so we can
  // use that directly.
  // TODO: Move this type conversion to the event priority module.
  const eventLane: Lane = (getCurrentEventPriority(): any);
  return eventLane;
}

最后会调用enqueueUpdate

export function enqueueUpdate<State>(
  fiber: Fiber,
  //更新链表
  update: Update<State>,
  lane: Lane,
) {
  const updateQueue = fiber.updateQueue;
  if (updateQueue === null) {
    // Only occurs if the fiber has been unmounted.
    return;
  }

  const sharedQueue: SharedQueue<State> = (updateQueue: any).shared;

  if (isInterleavedUpdate(fiber, lane)) {
    const interleaved = sharedQueue.interleaved;
    if (interleaved === null) {
      // This is the first update. Create a circular list.
      update.next = update;
      // At the end of the current render, this queue's interleaved updates will
      // be transferred to the pending queue.
      pushInterleavedQueue(sharedQueue);
    } else {
      update.next = interleaved.next;
      interleaved.next = update;
    }
    sharedQueue.interleaved = update;
  } else {
    const pending = sharedQueue.pending;
    if (pending === null) {
      // This is the first update. Create a circular list.
      update.next = update;
    } else {
      update.next = pending.next;
      pending.next = update;
    }
    sharedQueue.pending = update;
  }

  if (__DEV__) {
    if (
      currentlyProcessingQueue === sharedQueue &&
      !didWarnUpdateInsideUpdate
    ) {
      console.error(
        'An update (setState, replaceState, or forceUpdate) was scheduled ' +
          'from inside an update function. Update functions should be pure, ' +
          'with zero side-effects. Consider using componentDidUpdate or a ' +
          'callback.',
      );
      didWarnUpdateInsideUpdate = true;
    }
  }
}

setState的合并更新

数据的合并

我们通过setState修改state中的一个数据时,并不会把其他的数据覆盖掉而是会合并。

源码如下:

function getStateFromUpdate<State>(
  workInProgress: Fiber,
  queue: UpdateQueue<State>,
  update: Update<State>,
  prevState: State,
  nextProps: any,
  instance: any,
): any {
  switch (update.tag) {
    case ReplaceState: {
      const payload = update.payload;
      if (typeof payload === 'function') {
        // Updater function
        if (__DEV__) {
          enterDisallowedContextReadInDEV();
        }
        const nextState = payload.call(instance, prevState, nextProps);
        if (__DEV__) {
          if (
            debugRenderPhaseSideEffectsForStrictMode &&
            workInProgress.mode & StrictLegacyMode
          ) {
            setIsStrictModeForDevtools(true);
            try {
              payload.call(instance, prevState, nextProps);
            } finally {
              setIsStrictModeForDevtools(false);
            }
          }
          exitDisallowedContextReadInDEV();
        }
        return nextState;
      }
      // State object
      return payload;
    }
    case CaptureUpdate: {
      workInProgress.flags =
        (workInProgress.flags & ~ShouldCapture) | DidCapture;
    }
    // Intentional fallthrough
    case UpdateState: {
      const payload = update.payload;
      let partialState;
        //如果传入的参数为函数就不会合并执行
      if (typeof payload === 'function') {
        // Updater function
        if (__DEV__) {
          enterDisallowedContextReadInDEV();
        }
        partialState = payload.call(instance, prevState, nextProps);
        if (__DEV__) {
          if (
            debugRenderPhaseSideEffectsForStrictMode &&
            workInProgress.mode & StrictLegacyMode
          ) {
            setIsStrictModeForDevtools(true);
            try {
              payload.call(instance, prevState, nextProps);
            } finally {
              setIsStrictModeForDevtools(false);
            }
          }
          exitDisallowedContextReadInDEV();
        }
      } else {
        // Partial state object
        partialState = payload;
      }
      if (partialState === null || partialState === undefined) {
        // Null and undefined are treated as no-ops.
        return prevState;
      }
      // Merge the partial state and the previous state.
      //合并部分状态和前一个状态。
      return Object.assign({}, prevState, partialState);
    }
    case ForceUpdate: {
      hasForceUpdate = true;
      return prevState;
    }
  }
  return prevState;
}

可以看到内部是合并处理的。

本身的合并

如果我们多次调用setState修改state的值

//初始值为1
this.setState({
  counter:this.state.counter+1
})
this.setState({
  counter:this.state.counter+1
})
this.setState({
  counter:this.state.counter+1
})

实际上内部会将上述的setState合并为一次调用。内部会判断执行队列中所有setState的前一次的值和更新后的值是否相等,如果相等就会合并执行(这里发现都是从1变为2)。并不会执行后在之前的基础再次更新。

但是我们有时并不希望合并。

setState第一个参数可以传入一个函数。同时接收参数

  this.setState((preState, props) => {
      return {
        counter: preState.counter + 1,
      };
    });
    this.setState((preState, props) => {
      return {
        counter: preState.counter + 1,
      };
    });
    this.setState((preState, props) => {
      return {
        counter: preState.counter + 1,
      };
    });

上述操作就是可以累加。内部会判断setState传入的是对象还是函数,从而是否合并执行。

Last modification:March 7, 2022
如果觉得我的文章对你有用,请随意赞赏