前言
在React16.8
之前,React
通过this.state
来访问state
,通过this.setState()
方法来更新state
。当this.setState()
被调用的时候,React
会重新调用render
方法来重新渲染UI
。state
是React
中的重要概念。React
是通过状态管理来实现对组件的管理。那么,React
是如何控制组件的状态,又是如何利用状态来管理组件的呢?
一、合成事件中setState
在jsx
中常见的onClick
、onChange
这些都是合成事件
class App extends Component {
state = { val: 0 }
increment = () => {
this.setState({ val: this.state.val + 1 })
// 输出的是更新之前的val
console.log(this.state.val) // 0
}
render() {
return (
<div onClick={this.increment}>
{`Counter is: ${this.state.val}`}
</div>
)
}
}
当你在 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>
)
}
}
其实还是和合成事件一样,当 componentDidmount
执行的时候,react
内部并没有更新,执行完componentDidmount
后才去 commitUpdateQueue
更新。这就导致你在 componentDidmount
中 setState
完去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
这种绑定事件的形式都属于原生事件。
原生事件的调用栈就比较简单了,因为没有走合成事件的那一大堆,直接触发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
值。
五、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>
)
}
}
在 setState
的时候react
内部会创建一个 updateQueue
,通过 firstUpdate
、 lastUpdate
、 lastUpdate.next
去维护一个更新的队列,在最终的 performWork
中,相同的key
会被覆盖,只会对最后一次的 setState
进行更新。
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,而在 setTimmout
中 setState
是可以同步拿到更新结果,所以 setTimeout
中的两次输出2,3,最终结果就为 0, 0, 2, 3 。
总结
setState
只在合成事件和钩子函数中是“异步”的,在原生事件和setTimeout
中都是同步的。setState
的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数setState(partialState, callback)
中的callback
拿到更新后的结果。setState
的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout
中不会批量更新,在“异步”中如果对同一个值进行多次setState
,setState
的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时setState
多个不同的值,在更新时会对其进行合并批量更新。
原文地址: