React 的一次事务更新

这篇文章的由来是因为我在自己写的组件中,在 componentWillReceiveProps() 里面调用了 setState() 方法,导致 why-did-you-update 提示 state did not change. Avoidable re-render!

state did not change. Avoidable re-render!

官方也是推荐在 componentWillReceiveProps() 里面使用 setState(),照理说 state 都是变了, why-did-you-update 应该不会提示才对。为了一探究竟,决定去扒 React 的源代码,所以以下分析都是基于 React@15.6.2 来的。

先理一下思路,setState() 都干了啥,在开始之前先来看看几个问题。

  1. setState() 为什么是异步的 ?

  2. 为什么在 ReactUpdates.flushBatchedUpdates() 里面打断点,页面会没有任何反应。只有触发了第一次 flushBatchedUpdate() 后,再打断点程序才会正常执行 ?

    实际上是点击的时候触发的是 topFocus 事件,打断点之后不会再触发 topClick 事件

  3. 为什么 ReactEventListener.dispatchEvent() 会触发 ReactUpdates.batchedUpdates() ?

  4. 为啥 所有的更新操作都是在 Transaction.closeAll() 里面执行的 ?

  5. ReactUpdates.dirtyComponents 是咋来的 ?

  6. componentWillUpdate() 里面调用 setState() 会放入到当前的堆积更新中不 ?

先忽略这几个问题,先来看看 React 是怎么执行一次渲染的。假设我们点击了页面上的一个刷新按钮,由于 React 组件的事件都是通过 domcument 托管的,React 会在 document 上添加一个监听函数,由这个监听函数把事件分发给虚拟 DOM,这个监听函数就是 ReactEventListener.dispatchEvent() 它通过 ReactUpdates.batchedUpdates() 先启动批更新,然后通过事务来处理事件的分发与执行。启动批更新的目的,我猜是把所有这次事件触发的页面更新都放入一次批更新中。在事件执行过程中涉及的要更新的组件(一般都是调用 setState() 方法)都会通过 ReactUpdates.enquequeUpdate() 放入 dirtyComponents 队列中。在批更新事务结束的时候会调用 ReactUpdates.flushBatchedUpdates() 来检查 dirtyComponents 队列中是否有需要更新的组件。如果有就另起一个事务执行更新操作。启用事务的好处是,不管你在事务里面如何蹦跶我最后才来收拾你。

上面这个流程可以总结为两个阶段,一个队列阶段; 一个更新阶段。如果在更新阶段调用了 setState() 方法怎么办?React 会临时进入队列阶段,往 dirtyComponents 队列中加入组件。在更新阶段结束的时候会判断,如果 dirtyComponents 里面的组件数量大于已更新的的组件会再次进入更新阶段。这就解释了为什么在 componentWillReceiveProps() 里面调用了 setState(),会导致 why-did-you-update 提示 state did not change. Avoidable re-render!,因为确实更新了一次。难道说官方也有犯错的时候 ? 这确实造成了一次额外更新啊。如果给 this.state = nextState 直接赋值就会省掉多余的这次更新。

现在回到第一个问题,如果你在更新阶段(比如生命周期函数中)调用 setState(),那么铁定就是异步的了。如果在事件中调用 setState(),那么就要分是否是通过 React 的事件机制触发的。通过 React 事件机制触发的就是异步,其他方式( 比如dom.addEventListener() )就是同步的。

React enqueque and update

事情到这里好像就完了,事情也解释清楚了。但是在晚上对这篇文章复盘的时候,发现我自定义的组件,在页面是有四个的,但是 why-did-you-update 只提示了一个 state did not change. Avoidable re-render! 事情好像哪里不对,不是应该提示四个吗?为什么只提示了一个?

又去研究了一下源代码,发现在 componentWillUpdate() 里面调用 setState() 设置的 state 会在这次渲染的时候会处理掉。刚好提示的那个自定义组件在 setState() 前后 state 都是“一样的”,所以 why-did-you-update 才会报警。而再次进入的更新阶段实际上不会真的去执行更新,所以这里的这个报警是由上一个更新阶段触发的。不过 why-did-you-update 这里有点问题,由 props 触发的更新,但 state 刚好没有“变化”,就不应该提示 state did not change. Avoidable re-render!

我们再注意一下上面提到的用 this.state = nextState 直接赋值就会省掉多余的更新,由于是直接更改的 this.state,在后续的 shouldComponentUpdate() 里面 state 始终会被认为是未修改。如果你有 props 没变化而 state 需要改变这样的需求,那么在 componentWillReceiveProps() 最好是使用 this.setState() 来更新 state 而不是直接赋值。所以官方文档说的也是有道理的。

参考

componentWillReceiveProps()

batchedUpdates与事务

setState何时同步更新状态

为何说setState方法是异步的?

react源码解剖——setState的异步模型