Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React 的 setState #15

Open
mbaxszy7 opened this issue Jun 24, 2020 · 0 comments
Open

React 的 setState #15

mbaxszy7 opened this issue Jun 24, 2020 · 0 comments
Labels

Comments

@mbaxszy7
Copy link
Owner

记录下这么一个问题:React 中 setState 什么时候是同步的,什么时候是异步的?

这里的异步并不是异步执行,而是React会把多个setState合并更新

现象

直接上代码

class Test extends Component<
  Record<string, unknown>,
  { [propName: string]: number }
> {
  constructor(props: Record<string, unknown>) {
    super(props)
    this.state = {
      num: 1,
      number: 1,
      batchNumber: 1,
      clickNum: 1,
      callbackNum: 1
    }
  }

  componentDidMount(): void {
     // componentDidMount中正常的多次setState
    this.setState({
      num: this.state.num + 1
    })
    console.log(`num: ${this.state.num}`)
    this.setState({
      num: this.state.num + 2
    })
    console.log(`num: ${this.state.num}`)
    this.setState({
      num: this.state.num + 3
    })
    console.log(`num: ${this.state.num}`)

      // componentDidMount中在setTimeout内多次setState
    setTimeout(() => {
      this.setState({
        number: this.state.number + 1
      })
      console.log(`number: ${this.state.number}`)
      this.setState({
        number: this.state.number + 2
      })
      console.log(`number: ${this.state.number}`)
      this.setState({
        number: this.state.number + 3
      })
      console.log(`number: ${this.state.number}`)
    }, 0)

     // componentDidMount中在unstable_batchedUpdates内多次setState
    batchedUpdates(() => {
      this.setState({
        batchNumber: this.state.batchNumber + 1
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)

      this.setState({
        batchNumber: this.state.batchNumber + 2
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)
      this.setState({
        batchNumber: this.state.batchNumber + 3
      })
      console.log(`batchNumber: ${this.state.batchNumber}`)
    })

    // componentDidMount中在使用setState传入函数的多次setState
    this.setState((state) => ({
      callbackNum: state.callbackNum + 1
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
    this.setState((state) => ({
      callbackNum: state.callbackNum + 2
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
    this.setState((state) => ({
      callbackNum: state.callbackNum + 3
    }))
    console.log(`callbackNum: ${this.state.callbackNum}`)
  }

   // 在onClick 中多次setState
  handleTestClick: () => void = () => {
    this.setState((preState) => ({
      clickNum: preState.clickNum + 1
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
    this.setState((preState) => ({
      clickNum: preState.clickNum + 2
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
    this.setState((preState) => ({
      clickNum: preState.clickNum + 3
    }))
    console.log(`clickNum: ${this.state.clickNum}`)
  }

  render(): React.ReactElement {
    return (
      <>
        <h1 onClick={this.handleTestClick}> Test </h1>
        <p>clickNum {this.state.clickNum}</p>
        <p>callbackNum {this.state.callbackNum}</p>
        <p>batchNumber {this.state.batchNumber}</p>
        <p>number {this.state.number}</p>
        <p>num {this.state.num}</p>
      </>
    )
  }
}

上面代码注释有五种setState。下面来看一下log:
WeChatfc78152e23149c48c913843bd9746350
WeChatef96856db49289dc34c7d19a934e0a3e
五种setState在每次调用this.setState后的console.log中,只有setTimeout中的this.setState是每次都可以拿到最新的state的,其余都是原始的state值。

再来看一下屏幕上render的结果:
WeChat7102af3ef0d3773d8991654740c151ae
只有在setState中使用function 和 在setTimeout中的setState 才会根据上一次的state来产生state,其余都是用了原始值作为base state

原因

出现上面现象的原因主要有:

  1. 在setTimeout中setState不是批量更新的(所谓批量更新就是React通过一个queue来实现 state 更新,当执行 setState() 时,会将需要更新的 state 浅合并后放入 queue,而不会立即更新 state,队列机制可以高效的批量更新 state)
  2. 函数式 setState() 可以在传入的函数中拿到上一步的state

什么是React batched update 机制

在之前版本的React中会有一个isBatchingUpdates变量,当isBatchingUpdates 为true的时候会产生批量更新的效果(放到队列中),当isBatchingUpdates为false的时候会直接产生更新。比如在之前版本的React的unstable_batchedUpdates实现

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  // isBatchingUpdates 设置为true
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    // 复原isBatchingUpdates 
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

在当前最新版本的React(16.13.1)中unstable_batchedUpdates的实现:

function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  executionContext |= BatchedContext;
  try {
    return fn(a);
  } finally {
    executionContext = prevExecutionContext;
    if (executionContext === NoContext) {
      // Flush the immediate callbacks that were scheduled during this batch
      flushSyncCallbackQueue();
    }
  }
}

可以看到React已经去掉了isBatchingUpdates,换成了executionContext这个枚举值

然后再看当在setTimeout中setState的时候executionContext会变成0 (枚举值NoContext):

// scheduleUpdateOnFiber也就是scheduleWork (在enqueueSetState中调用)
function scheduleUpdateOnFiber(
  fiber: Fiber,
  lane: Lane,
  eventTime: number,
) {
  checkForNestedUpdates();
  warnAboutRenderPhaseUpdatesInDEV(fiber);

  const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  if (root === null) {
    warnAboutUpdateOnUnmountedFiberInDEV(fiber);
    return null;
  }

  // TODO: requestUpdateLanePriority also reads the priority. Pass the
  // priority as an argument to that function and this one.
  const priorityLevel = getCurrentPriorityLevel();

  if (lane === SyncLane) {
    if (
      // 是否在unbatched update中
      (executionContext & LegacyUnbatchedContext) !== NoContext &&
      (executionContext & (RenderContext | CommitContext)) === NoContext
    ) {
      schedulePendingInteractions(root, lane);
      performSyncWorkOnRoot(root);
    } else {
      ensureRootIsScheduled(root, eventTime);
      schedulePendingInteractions(root, lane);
      // setTimeout命中,触发flushSyncCallbackQueue
      if (executionContext === NoContext) {
        flushSyncCallbackQueue();
      }
    }
  } else {
    if (
      (executionContext & DiscreteEventContext) !== NoContext &&
      (priorityLevel === UserBlockingSchedulerPriority ||
        priorityLevel === ImmediateSchedulerPriority)
    ) {
      if (rootsWithPendingDiscreteUpdates === null) {
        rootsWithPendingDiscreteUpdates = new Set([root]);
      } else {
        rootsWithPendingDiscreteUpdates.add(root);
      }
    }
    ensureRootIsScheduled(root, eventTime);
    schedulePendingInteractions(root, lane);
  }
  mostRecentlyUpdatedRoot = root;
}

总结setState合并执行

  • 在当前最新的React版本(16.13.1)中,如下情况会有setState batched的情况:
  1. React 组件的合成事件回调
  2. ReactDOM.unstable_batchedUpdates
  3. componentDidMount 和 useEffect
  • 没有setState batched的情况:
  1. 异步函数,如setTimeout, setInterval,async/await等
  2. addEventListener 内
  3. 异步回调
@mbaxszy7 mbaxszy7 added the react label Jun 24, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant