你真的理解setState吗?

面试官:“react中 setState是同步的还是异步?”
我:“异步的, setState不能立马拿到结果。” 

面试官:“那什么场景下是异步的,可不可能是同步,什么场景下又是同步的?”
我:“......”

setState真的是异步的吗 ?

这两天自己简单的看了下setState的部分实现代码,在这边给到大家一个自己个人的见解,可能文字或图片较多,没耐心的同学可以直接跳过看总结(源码版本是16.4.1)。

看之前,为了方便理解和简化流程,我们默认react内部代码执行到performWorkperformWorkOnRootperformSyncWorkperformAsyncWork这四个方法的时候,就是react去update更新并且作用到UI上。

一、合成事件中的setState

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

class App extends Component {

  state = { val: 0 }

  increment = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新前的val --> 0
  }
  render() {
    return (
      <div onClick={this.increment}>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

合成事件中的setState写法比较常见,点击事件里去改变this.state.val的状态值,在increment事件中打个断点可以看到调用栈,这里我贴一张自己画的流程图:

合成事件中setState的调用栈



从 dispatchInteractiveEvent 到 callCallBack 为止,都是对合成事件的处理和执行,从 setState 到 requestWork 是调用 this.setState 的逻辑,这边主要看下 requestWork 这个函数(从 dispatchEvent 到 requestWork 的调用栈是属于 interactiveUpdates$1 的 try 代码块,下文会提到)。

function requestWork(root, expirationTime) {
  addRootToSchedule(root, expirationTime);

  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, false);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpiration(expirationTime);
  }
}

requestWork中有三个if分支,三个分支中有两个方法 performWorkOnRoot 和performSyncWork,就是我们默认的update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdates 和 isUnbatchingUpdates 两个初始值都为false,但是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:

function interactiveUpdates$1(fn, a, b) {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPendingInteractiveExpirationTime, false, null);
    lowestPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;  // 把requestWork中的isBatchingUpdates标识改为true
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

在这个方法中把 isBatchingUpdates 设为了 true , 导致在 requestWork 方法中, isBatchingUpdates 为 true ,但是 isUnbatchingUpdates 是 false,而被直接return了。

那return完的逻辑回到哪里呢,最终正是回到了 interactiveUpdates 这个方法,仔细看一眼,这个方法里面有个 try finally 语法,前端这个其实是用的比较少的,简单的说就是会先执行 try代码块中的语句,然后再执行 finally 中的代码,而 fn(a, b) 是在try代码块中,刚才说到在requestWork 中被return掉的也就是这个fn(上文提到的 从dispatchEvent到 requestWork 的一整个调用栈)。

所以当你在increment中调用 setState 之后去 console.log 的时候,是属于try代码块中的执行,但是由于是合成事件,try 代码块执行完 state 并没有更新,所以你输入的结果是更新前的 state 值,这就导致了所谓的"异步",但是当你的 try 代码块执行完的时候(也就是你的increment合成事件),这个时候会去执行 finally 里的代码,在 finally 中执行了 performSyncWork 方法,这个时候才会去更新你的 state并且渲染到UI上。

二、生命周期函数中的setState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 输出的还是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

钩子函数中setState的调用栈:

钩子函数中setState的调用栈



其实还是和合成事件一样,当componentDidmount执行的时候,react内部并没有更新,执行完componentDidmount后才去commitUpdateQueue更新。这就导致你在componentDidmountsetState完去console.log拿的结果还是更新前的值。

三、原生事件中的setState

class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

原生事件是指非react合成事件,原生自带的事件监听 addEventListener ,或者也可以用原生js、jq直接 document.querySelector().onclick 这种绑定事件的形式都属于原生事件。

原生事件中setState的调用栈



原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发click事件,到requestWork ,在 requestWork 里由于 expirationTime === Sync 的原因,直接走了 performSyncWork 去更新,并不像合成事件或钩子函数中被return,所以当你在原生事件中setState后,能同步拿到更新后的state值。

四、setTimeout中的setState

class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 输出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

在 setTimeout 中去 setState 并不算是一个单独的场景,它是随着你外层去决定的,因为你可以在合成事件中 setTimeout,可以在钩子函数中 setTimeout,也可以在原生事件setTimeout,但是不管是哪个场景下,基于event loop的模型下,setTimeout 中里去 setState 总能拿到最新的state值。

举个栗子,比如之前的合成事件,由于 setTimeout(_ => { this.setState()}, 0)是在 try 代码块中,当你 try 代码块执行到setTimeout的时候,把它丢到列队里,并没有去执行,而是先执行的 finally 代码块,等 finally 执行完了,isBatchingUpdates又变为了 false,导致最后去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支,所以表现就会和原生事件一样,可以同步拿到最新的state值。

五、setState中的批量更新

class App extends Component {

  state = { val: 0 }

  batchUpdates = () => {
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
    this.setState({ val: this.state.val + 1 })
 }

  render() {
    return (
      <div onClick={this.batchUpdates}>
        {`Counter is ${this.state.val}`} // 1
      </div>
    )
  }
}

上面的结果最终是1,在setState的时候react内部会创建一个updateQueue,通过firstUpdatelastUpdatelastUpdate.next去维护一个更新的队列,在最终的performWork中,相同的key会被覆盖,只会对最后一次的setState进行更新,下面是部分实现代码:

function createUpdateQueue(baseState) {
  var queue = {
    expirationTime: NoWork,
    baseState: baseState,
    firstUpdate: null,
    lastUpdate: null,
    firstCapturedUpdate: null,
    lastCapturedUpdate: null,
    firstEffect: null,
    lastEffect: null,
    firstCapturedEffect: null,
    lastCapturedEffect: null
  };
  return queue;
}

function appendUpdateToQueue(queue, update, expirationTime) {
  // Append the update to the end of the list.
  if (queue.lastUpdate === null) {
    // Queue is empty
    queue.firstUpdate = queue.lastUpdate = update;
  } else {
    queue.lastUpdate.next = update;
    queue.lastUpdate = update;
  }
  if (queue.expirationTime === NoWork || queue.expirationTime > expirationTime) {
    // The incoming update has the earliest expiration of any update in the
    // queue. Update the queue's expiration time.
    queue.expirationTime = expirationTime;
  }
}

最后看个

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值