近來在學習react源碼, 最初是直接從入口一行一行的看, 結果跟着調用的函數跳轉來跳去頭都暈了. 后來決定帶着一個目的去看源碼, 每次看只研究一個東西. 一開始最想了解的就是充滿魔性的setState. 本文是我對setState的一些理解, 不當之處歡迎留言指正.
setState的魔性
看一下下邊幾個例子的輸出情況.
例1 合成事件中的setState
import React from 'react';
export default class SetState extends React.Component {
constructor(props) {
super(props);
}
state = {
count: 0
}
click = () => {
this.setState({
count: this.state.count + 1,
})
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
}
render() {
return (
<div onClick={this.click}>
count的值{this.state.count}
</div>
)
}
}
// 打印:
// count1 0
// count2 0
例2 生命周期函數中的setState
import React from 'react';
export default class SetState extends React.Component {
constructor(props) {
super(props);
}
state = {
count: 0
}
componentDidMount () {
this.setState({
count: this.state.count + 1,
})
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
}
render() {
return (
<div>
count的值{this.state.count}
</div>
)
}
}
// 打印:
// count1 0
// count2 0
例3 setTimeout中的setState
import React from 'react';
export default class SetState extends React.Component {
constructor(props) {
super(props);
}
state = {
count: 0
}
componentDidMount () {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
})
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
}, 0);
}
render() {
return (
<div>
count的值{this.state.count}
</div>
)
}
}
// 打印:
// count1 1
// count2 2
例4 Promise中的setState
import React from 'react';
export default class SetState extends React.Component {
constructor(props) {
super(props);
}
state = {
count: 0
}
componentDidMount () {
Promise.resolve()
.then(() => {
this.setState({
count: this.state.count + 1,
})
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
})
}
render() {
return (
<div>
count的值{this.state.count}
</div>
)
}
}
// 打印:
// count1 1
// count2 2
從例1和例2的輸出結果來看, 在setState后直接取state的值發現並沒有更新, setState對state的更新似乎是個異步的過程;
而從例3, 例4輸出結果來看, setState又是一個同步更新state的操作, 可以立即拿到更新的結果.
也就是說, setState有的時候是異步的有的時候是同步的, 真是非常的魔性. 根據網上的一些文章和自己的實驗可以得出如下結論.
- 在合成事件, 生命周期函數中的setState是異步批量更新的, 不能立即拿到更新的結果, 多次setState只會走一次render
- 在setTimeOut, setInterval, 原生事件, Promise中的setState是同步逐個更新的, 可以立即拿到更新的state, 而且每次setState都會走一次render
關於是批量更新還是非批量更新可以在render函數中打印查看
setState魔性表現揭秘
理解setState的異步批量更新
下邊是個異步批量更新的示意圖
這里將在合成事件, setTimeout等中的寫的代碼的調用稱為Main Process.
例如下邊componentDidMount
中的代碼的執行都叫Main process.
componentDidMount () {
this.setState({
count: this.state.count + 1,
});
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
}
直接結合這段代碼分析上邊的這個看起來很牛x的圖.
首先執行一個setState, 判斷是setState操作, 創建一個更新任務加入更新隊列, 交給協調中心, 協調中心判斷不需要更新, 繼續執行main Process中的代碼.
遇到第一個console, 直接執行, 打印時取出了state, 顯然state沒更新還是原來的值, 然后再執行Main Process代碼.
遇到第二個setState, 注意此時取出的state是沒有更新的, 再創建一個更新任務到更新隊列, 交給協調中心, 協調中心判斷不需要更新, 繼續執行main Process中的代碼. 然后執行了console, 取出的state是沒更新的.
一定時間后, 協調中心再次調度, 發現可以更新了, 然后執行了更新隊列的兩個任務, 得到一個新的state, 然后更新this.state
和視圖.
從以上分析可以了解到為什么兩個console打印的都是之前的值.
這里有一個黑盒, 協調中心怎么運行的, 這是以后需要研究的了, 目前尚不清楚, 可以猜測這里邊應該有個setTimeout 或者類似setTimeout的東西.
理解setState的同步單個更新
下邊是同步更新的示意圖
這里還是結合一段代碼來分析
import React from 'react';
export default class SetState extends React.Component {
constructor(props) {
super(props);
}
state = {
count: 0
}
click = () => {
setTimeout(() => {
this.setState({
count: this.state.count + 1,
})
console.log('count1', this.state.count);
this.setState({
count: this.state.count + 1,
});
console.log('count2', this.state.count);
}, 0);
}
render() {
return (
<div onClick={this.click}>
count的值{this.state.count}
</div>
)
}
}
首先遇到第一個setState, 判斷是setState, 創建一個更新任務到更新隊列, 然后進入協調中心, 協調中心通過某種手段判斷出需要同步更新, 直接執行更新隊列的任務, 得到新的state, 然后更新視圖, 繼續執行Main Process中的代碼.
遇到console, 直接執行, 取出state(注意是更新了的)答應.
然后又遇到setState(注意這里拿到的state是更新了的), 創建更新任務進入更新隊列, 然后進入協調中心, 協調中心通過某種手段判斷出需要同步更新, 直接執行更新隊列的任務, 得到新的state, 然后更新視圖, 繼續執行Main Process中的代碼.
再次遇到console, 直接執行, 取出state(注意是二次了的)答應.
從以上分析可以看出同步setState
為什么是同步的, 原因就在於他沒有一個異步判斷過程, 直接更新了state.
幾點待解決的問題
- 協調中心是什么時候, 如何判斷出需要更新的
- 協調中心是如何識別是一個setState是在setTimeout還是在合成事件亦或生命周期等過程中的.
彩蛋
說一下閱讀react源碼的感受, 最開始直接看src目錄, react部分還行, 比較容易.
但是到了react-dom
就不行了, 各種調用, 各種亂七八糟的東西, 有時跟着函數調用跳來跳去, 結果最開始想干嘛的都忘了, 這樣讀起來真的很打擊人.
其實讀源碼更多不是了解其代碼組織方法, 而是了解核心原理.
下邊是幾個小建議:
- 帶着問題讀源碼, 尤其是開始讀的時候, 如果漫無目的的讀, 會很沒有成就感, 甚至是強烈的挫敗感, 讀了半天也不知道學到了什么
react-dom
的src代碼組織十分復雜, 建議直接讀開發版的編譯產物, 都在一個文件里, 比較容易找.- 多用斷點, 可以直接在開發版編譯產物打斷點看, 非常方便
- 不要糾結太多細節, 要抱有不求甚解的態度, 不懂的地方可以暫時放過
小結
setState是一個容易讓人困惑的東西, 尤其對react初學者來說, 可能感覺有點琢磨不透. 本文結合源碼和自己的理解對setState的同步異步機制做了一些分析. 有些地方可能並不是十分准確, 但希望能幫助對setState同步異步機制困惑的朋友理解一些其中的原理. 最后需要記憶一下什么場景是同步更新, 什么場景是異步更新, 這個是寫代碼能實實在在用的到的.