这篇文章的由来是因为我在自己写的组件中,在 componentWillReceiveProps()
里面调用了 setState()
方法,导致 why-did-you-update 提示 state did not change. Avoidable re-render!
官方也是推荐在 componentWillReceiveProps()
里面使用 setState()
,照理说 state
都是变了, why-did-you-update 应该不会提示才对。为了一探究竟,决定去扒 React 的源代码,所以以下分析都是基于 React@15.6.2
来的。
先理一下思路,setState()
都干了啥,在开始之前先来看看几个问题。
setState()
为什么是异步的 ?为什么在
ReactUpdates.flushBatchedUpdates()
里面打断点,页面会没有任何反应。只有触发了第一次flushBatchedUpdate()
后,再打断点程序才会正常执行 ?实际上是点击的时候触发的是 topFocus 事件,打断点之后不会再触发 topClick 事件
为什么
ReactEventListener.dispatchEvent()
会触发ReactUpdates.batchedUpdates()
?为啥 所有的更新操作都是在
Transaction.closeAll()
里面执行的 ?ReactUpdates.dirtyComponents
是咋来的 ?在
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() )就是同步的。
事情到这里好像就完了,事情也解释清楚了。但是在晚上对这篇文章复盘的时候,发现我自定义的组件,在页面是有四个的,但是 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
而不是直接赋值。所以官方文档说的也是有道理的。