1. 不能直接設置this.state
這個基本學習過 react 的讀者都不會犯這樣的錯,直接設置 this.state 的值並不能觸發組件 render()
,正確的是調用 setState()
函數來處理。
2. setState()
回調函數
我們在用 setState()
時,疑惑比較多的地方就是 setState()
可能不會立即生效。基於這一點,慢慢地我們就形成了 setState()
總是異步執行的假象。其實官方文檔裏也說的是可能不會立即生效
,後面會講到立即生效的場景。其實 setState()
會批量推遲更新。這使得在調用 setState() 後立即讀取 this.state 成爲了隱患。例如以下示例:
hanldClick = () => {
// 初始counter=0
this.setState({
counter: this.state.counter + 1
});
console.log(this.state.counter); // 0
};
在 setState()
之後直接調用取 counter
的值的話,其實取到的還是生效之前的值,如果我們想要在 counter
生效之後取值的話,可以使用 componentDidUpdate 或者 setState 的回調函數(setState(updater, callback)),這兩種方式都可以保證在應用更新後觸發。我們常用的方式就是 setState 的第二個入參(回調函數)。上述的示例就可以修改成如下:
hanldClick = () => {
// 初始counter=0
this.setState({
counter: this.state.counter + 1
},()=>{
console.log(this.state.counter); // 1
});
};
如果在 render 中也加上打印信息,可以發現,上述的打印信息會在 render 之後打印。
3. setState()
批量執行
我們將上面的示例變更一下:
hanldClick = () => {
this.setState(
{
counter: this.state.counter + 1
},
() => {
console.log(this.state.counter);
}
);
this.setState(
{
counter2: this.state.counter2 + 2
},
() => {
console.log(this.state.counter2);
}
);
};
在 render 裏也加上打印信息,我們可以發現上述操作並不會出發兩次 render ,而是將兩次 setState()
合併在一起執行了,與下面是等效的:
hanldClick = () => {
this.setState(
{
counter: this.state.counter + 1,
counter2: this.state.counter2 + 2
},
() => {
console.log(this.state.counter);
console.log(this.state.counter2);
}
);
};
再看下面一個示例:
hanldClick = () => {
// 初始counter=0
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
this.setState({
counter: this.state.counter + 1
});
};
上述操作生效後,counter 的值爲 1,而不是 3。這是因爲 setState()
不一定是立即生效的,而且是批量執行,所以上述的 三個 setState()
中 拿到的 this.state.counter
值都是 0,所以合併起來就等效與:
hanldClick = () => {
// 初始counter=0
this.setState({
counter: this.state.counter + 1
});
};
如果要實現上述的操作,可以採用以下 setState()
第一個入參爲函數的方法。
4. setState()
第一個入參爲函數
從[官方文檔(https://zh-hans.reactjs.org/docs/react-component.html#setstate)中可知,setState()
第一個參數除了可以傳一個對象(nextState,nextState會與當前state做淺 merge 操作),還可以傳函數,該函數中接收的 state 和 props 都保證爲最新,且返回值會與 state 進行淺 merge 操作。:
void setState (
function|object nextState,
[function callback]
)
因此要實現上述的場景,就可以改寫成如下:
hanldClick = () => {
// 初始counter=0
this.setState((state,props)=>({
counter: state.counter + 1
}),()=>{
console.log(this.state.counter); // 3
});
this.setState((state, props) => ({
counter: state.counter + 1
}),()=>{
console.log(this.state.counter); // 3
});
this.setState((state, props) => ({
counter: state.counter + 1
}),()=>{
console.log(this.state.counter); // 3
});
console.log(this.state.counter); // 0
};
5. 原生事件中修改狀態
先看一個示例:
class Index extends Component {
constructor(props) {
super();
this.state = {
counter: 0,
counter2: 0
};
}
componentDidMount() {
let elem = document.getElementById("btn");
elem.addEventListener("click", this.changeValue, false);
}
changeValue = () => {
this.setState({
counter: this.state.counter+1
});
console.log(this.state.counter)
}
hanldClick = () => {
this.setState({
counter: this.state.counter + 1
});
console.log(this.state.counter)
};
render() {
console.log("======== indexPage render ========");
console.log(this.state);
return (
<div>
<p>counter: {this.state.counter}</p>
<button onClick={this.hanldClick}>增加</button>
<button id="btn">原生事件</button>
</div>
);
}
}
通過原生事件方式添加的點擊事件中 changeValue
中的打印信息可以直接打印出變更之後的 counter
, 而通過react的 onClick
事件中的打印信息卻還是變更之前的值。
在 React 的 setState 函數實現中,會根據一個變量 isBatchingUpdates 判斷是直接更新 this.state 還是放到隊列中回頭再說,而 isBatchingUpdates 默認是 false,也就表示 setState 會同步更新 this.state,但是,有一個函數 batchedUpdates,這個函數會把 isBatchingUpdates 修改爲 true,而當 React 在調用事件處理函數之前就會調用這個 batchedUpdates,造成的後果,就是由 React 控制的事件處理過程 setState 不會同步更新 this.state。
-
也就是說上述的
onClick
是 React 控制的事件處理過程,所以 isBatchingUpdates 修改爲 true,導致不會同步更新this.state; -
而添加的原生事件不會修改 isBatchingUpdates 的值,還是默認 false, 所以會同步更新 this.state。
6. setTimeout
我們在上面的 hanldClick 中加一個 setTimeout 邏輯:
hanldClick = () => {
// 初始counter=0
this.setState({
counter: this.state.counter + 1
});
console.log(this.state.counter); // 0
setTimeout(() => {
this.setState({
counter: this.state.counter + 1
});
console.log(this.state.counter); // 2
this.setState({
counter: this.state.counter + 1
});
console.log(this.state.counter); // 3
}, 0);
};
上述第一個 setState()
之後打印 0 的原因上面已經提到過了。至於後面 setTimeout
中的打印結果,可能會有一些疑惑。其實還是 isBatchingUpdates 值的原因,因爲 setTimeout 裏面的回調函數已經不是是 React 控制的事件處理過程了, isBatchingUpdates 的值還是默認的 false, 所以會同步更新 this.state。
reference:
1、https://zh-hans.reactjs.org/docs/react-component.html#setstate