你真的理解setState吗?

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

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

setState真的是异步的吗?

这两天本身简单的看了下 setState 的部分实现代码,在这边给到你们一个本身我的的看法,可能文字或图片较多,没耐心的同窗能够直接跳过看总结(源码版本是16.4.1)。前端

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

1、合成事件中的setState

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

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 事件中打个断点能够看到调用栈,这里我贴一张本身画的流程图:git

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

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分支,三个分支中有两个方法 performWorkOnRootperformSyncWork ,就是咱们默认的update函数,可是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdatesisUnbatchingUpdates 两个初始值都为 false ,可是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:github

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 方法中, isBatchingUpdatestrue ,可是 isUnbatchingUpdatesfalse ,而被直接return了。面试

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

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

2、生命周期函数中的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的调用栈:

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

3、原生事件中的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 这种绑定事件的形式都属于原生事件。

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

4、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值。

5、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;
  }
}
复制代码

看个🌰

class App extends React.Component {
  state = { val: 0 }

  componentDidMount() {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val)

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

    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val);

      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val)
    }, 0)
  }

  render() {
    return <div>{this.state.val}</div>
  }
}
复制代码

结合上面分析的,钩子函数中的 setState 没法立马拿到更新后的值,因此前两次都是输出0,当执行到 setTimeout 里的时候,前面两个state的值已经被更新,因为 setState 批量更新的策略, this.state.val 只对最后一次的生效,为1,而在 setTimmoutsetState 是能够同步拿到更新结果,因此 setTimeout 中的两次输出2,3,最终结果就为 0, 0, 2, 3

总结 :

  1. setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  2. setState的“异步”并非说内部由异步代码实现,其实自己执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新以前,致使在合成事件和钩子函数中无法立马拿到更新后的值,形式了所谓的“异步”,固然能够经过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  3. setState 的批量更新优化也是创建在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中若是对同一个值进行屡次 setStatesetState 的批量更新策略会对其进行覆盖,取最后一次的执行,若是是同时 setState 多个不一样的值,在更新时会对其进行合并批量更新。

以上就是我看了部分代码后的粗浅理解,对源码细节的那块分析的较少,主要是想让你们理解setState在不一样的场景,不一样的写法下到底发生了什么样的一个过程和结果,但愿对你们有帮助,因为是我的的理解和看法,若是哪里有说的不对的地方,欢迎你们一块儿指出并讨论。

另外,帮朋友打个广告 :

有好友整理了一波内推岗位,已发布到github,感兴趣的能够联系cXE3MjcwNDAxNDE=

相关文章
相关标签/搜索