前言
state
是React
中的重要概念。我们知道,React
是通过状态管理来实现对组件的管理。那么,React
是如何控制组件的状态的,又是如何利用状态来管理组件的呢?
我们都知道,React
通过this.state
来访问state
,通过this.setState()
方法更新state
。当this.setState()
被调用的时候,React
会重新调用render
方法来重新渲染UI
。
setState
已经是我们非常熟悉的一个API
,然而你真的了解它吗?下面我们将一起来解密setState
的更新机制。
setState异步更新
大家刚开始写React
的时候,通常会写出 this.state.value = 1
这样的代码,这是完全错误的写法。
注意:绝对不要直接修改 this.state,这不仅是一种低效的做法,而且很有可能会被之后的操作替换。
setState
通过一个队列机制实现state
更新。当执行setState
时,会将需要更新的state
合并后放入状态对列,而不会立刻更新this.state
,队列机制可以高效地批量更新state
。如果不通过setState
而直接修改this.state
的值,那么该state
将不会被放入状态队列中,当下次调用setState
并对状态队列进行合并时,将会忽略之前直接被修改的 state
,而造成无法预知的错误。
因此,应该使用 setState
方法来更新 state
,同时 React
也正是利用状态队列机制实现了 setState
的异步更新,避免频繁地重复更新 state
。相关代码如下:
// 将新的state合并到状态更新队列中
var nextState = this._processPendingState(nextProps, nextContext);
// 根据更新队列和 shouldComponentUpdate 的状态来判断是否需要更新组件
var shouldUpdate = this._pendingForceUpdte || !inst.shouldCompoonentUpdate || inst.shouldComponentUpdate(nextProps, nextState, nextContext0;
那么,你知道在哪些情况下,会有类似于同步的更新吗?
setState循环调用风险
我们常见的循环调用的错误场景如下图:
当调用setState
时,实际上会执行 enqueueSetState
(enqueue: 队列) 方法,并对 partialState
(partial: 部分的) 以及 _pendingStateQueue
更新队列进行合并操作,最终操作 enqueueSetState
执行 state
更新。
而 performUpdateIfNecessary
方法会获取 _pendingElement、_pendingStateQueue、_pendingForceUpdate
,并调用 receiveComponent
和 updateComponent
方法进行组件更新。
如果在 shouldComponetUpdate
或 componentWillUpdate
方法中调用 setState
, 此时 this._pendingStateQueue != null
, 则 performUpateIfNecessary
方法就会调用 updateComponent
方法进行组件更新,但 updateComponent
方法又会调用 shouldComponentUpdate
和 componentWillUpdate
方法,因此造成循环调用
,使得浏览器内存占满后崩溃。
setState
源码如下:
// 更新state
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if(callback) {
this.updater.enqueueCallback(this, callback, 'setState')
}
};
enqueueSetState: function(publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if(!internalInstance) {
return;
}
// 更新队列合并操作
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
queue.push(partialState);
enqueueUpdate(internalInstance);
},
// 如果存在 _pendingElement、_pendingStateQueue和_pendingForceUpdate,则更新组件
performUpdateIfNecessary: function(transaction) {
if(this._pendingElement !== null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
}
if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
}
}
setState调用栈
既然 setState
最终是通过 enqueueUpate
执行 state
更新,那么 enqueueUpdate
到底是如何更新 state
的呢?
首先,看看下面这个问题,你是否能够正确回答呢?
import React, { Component } from 'react'
class Example extends Component {
constructor() {
super()
this.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 null
}
}
上述代码中, 4 次 console.log
打印出来的 val
分别是:0、0、2、3
。(后边为什么是2 、3
,而不是3、4
呢 ?因为在前两次的setState
中,this.state.val
取值都为0
,还没有变更为1
,导致虽然赋值两次,但其实赋的两次值都为1
,因此导致setTimeout
的值为2、3
)
假如结果与你心中的答案不完全相同,那么你是否想知道 enqueueUpdate
到底做了什么?
下图是一个简化的 setState
调用栈,注意其中核心的状态判断。
setState
简化调用栈
enqueueUpdate
的代码如下:
function enqueueUpdate(component) {
ensureInjected();
// 如果不处于批量更新模式
if(!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchUpdates(enqueueUpdate, component)l
return;
}
// 如果处于批量更新模式,则将该组件保存在 dirtyComponents 中
dirtyComponents.push(component);
}
如果isBatchingUpdates
为true
, 则对所有队列中的更新执行batchedUpdates
方法,否则,只有把当前组件(即调用了setState
的组件)放入 dirtyComponent
数组中。上述例子中的4次setState
调用之后 console
的表现之所以不同,就是因为这里的逻辑判断起了关键作用。
解密setState
到底是怎么导致 setState
的各种不同表现的呢?
首先,我们把4次 setState
简单归类,前两次属于一类,因为他们在同一次调用栈中执行,setTimeout
中的两次 setState
属于另一类,因为他们也是在同一次调用栈中执行。我们分析一下这两类 setState
的调用栈。
在 componentDidMount
中直接调用的两次 setState
,其调用栈更加复杂;而setTimeout
中调用的两次 setState
,其调用栈则简单很多。下面我们重点看看第一类 setState
的调用栈,我们发现了 batchedUpdates
方法,原来早在 setState
调用前,已经处于batchedUpdates
执行的事务中了。
那batchedUpdates
方法,又是谁调用的呢?我们再往前追溯一层,原来是 ReactMount.js 中的 _renderNewRootComponent
方法。也就是说,整个将React组件渲染到DOM中的过程就处于一个大的事务中。
接下来的解释就顺理成章了,因为在componentDidMount
中调用setState
时,batchingStrategy
的 isBatchingUpdates
已经被设为true
,所以两次setState
的结果并没有立即生效,而是被放到了dirtyComponents
中。这也解释了两次打印 this.state.val
都是 0
的原因,因为新的 state
还没有被应用到组件中。
componentDidMount
中setState
的调用栈
setTimeout
中setState
的调用栈
再反观 setTimeout
中的两次setState
,因为没有前置的 batchedUpdate
调用,所以 batchingStrategy
的 isBatchingUpates
标志位是false
,也就导致了新的 state
马上生效,没有走到 dirtyComponents
分支。也就是说,setTimeout
中第一次执行 setState
时,this.state.val
为 1
, 而 setState
完成打印后打印时 this.state.val
变成了2
。第二次的 setState
同理。
在 React 15.0
之前的版本中还是为开发者提供了 batchedUpdates
方法,它可以解决针对一开始例子中setTimeout
里的两次 setState
导致两次 render
的情况:
import ReactDOM, { unstable_batchedUpates } from 'teact-dom'
unstable_batchedUpates(() => {
this.setState(val: this.state.val + 1)
this.setState(val: this.state.val + 1)
})
在 React 15.0
以及之后版本中,已经彻底将 batchUpdates
这个 API
移除了,因此不再建议开发者使用它。
总结
在使用React
的setState
的过程中,了解setState
的实现原理,对setState
异步更新、setState
循环调用风险、setState
调用栈等进行更加全面的了解,才能让我们在遇到相关问题的时候更加游刃有余。