問一個react更新State的問題?
讀react官網:
狀態更新可能是動態的
// Wrong
this.setState({
counter: this.state.counter + this.props.increment,
});
官網說這種寫法是錯誤的
// Correct
this.setState((prevState, props) =&> ({
counter: prevState.counter + props.increment
}));
這種寫法是正確的
我實在搞不懂為什麼第一個是錯誤的,第二種寫法是正確的,哪位大神能幫忙解釋一下?在哪種需求場景下,會出現上述的情況,最好能寫點代碼解釋下,多謝,大神們指導。
這個問題確實像其他回答評價的那樣,「老生常談」了。
但是有兩點我不是很理解:
- 為什麼其他回答都是上來就說「React setState 更新是非同步的」?(錯誤的誤導)
- 為什麼在很多答主本身都並沒有完全理解這個問題的情況下,就「教育」「嘲諷」題主自己查資料、解決問題?
先說第二點,如果所有人都具備親自手動查資料解決問題的能力,那麼社區上是不是就會少太多「愚蠢」的問題和針對幼稚問題的詳盡答案。
作為開發者,我們一定要具備自學自研的能力。同時,時刻都要 be humble,「冷嘲熱諷他人提問,數落他人不會自己查問題解決」不是很好的表現。
「有問題,儘管問」才會有高質量的回答,才會有其他人「自行查詢」解決的前提。
再來說第一點,回到問題的本身。我企圖從 React 官方找到問題的答案:
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState()a potential pitfall.
Voilà, 準確的用詞是 batch later 或者 defer, 這種表達和我們傳統上所謂的「非同步」不盡相同。另外,人家用詞明顯是 may,因此我們知道這種所謂的「延遲更新」並不 cover 100% cases。換句話說,某些情況下,它也是能夠馬上或者「同步」完成更新的。
那麼,怎麼就像其他答主所說的那樣,一定就是非同步更新了呢?
There is no guarantee that this.state will be immediately updated, so
accessing this.state after calling this method may return the old value.
首先,不保證馬上更新是真的,比如我們試驗這樣的代碼:
function incrementMultiple() {
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
this.setState({count: this.state.count + 1});
}
直觀上來看,當上面的 incrementMultiple 函數被調用時,組件狀態的 count 值被增加了3次,每次增加1,那最後 count 被增加了3。但是,實際上的結果只給 state 增加了1。
事實上,
setState 方法與包含在其中的執行是一個很複雜的過程,從 React 最初的版本到現在,也有無數次的修改。它的工作除了要更動 this.state 之外,還要負責觸發重新渲染,這裡面要經過 React 核心 diff 演算法,最終才能決定是否要進行重渲染,以及如何渲染。而且為了批次與效能的理由,多個 setState 呼叫有可能在執行過程中還需要被合併,所以它被設計以延時的來進行執行是相當合理的。
但是,你再試試:
componentDidMount() {
document.querySelector("#btn-raw").addEventListener("click", this.onClick);
}
onClick() {
this.setState({count: this.state.count + 1});
console.log("# this.state", this.state);
}
// ......
render() {
console.log("#enter render");
return (
&
&{this.state.count}
&
&
}
你會發現,控制台又列印出了準確的 count 值,進行了同步更新!
(代碼示例和引用部分來自 @程墨Morgan 文章 setState何時同步更新狀態)
深入源碼你會發現:
在 React 的 setState 函數實現中,會根據一個變數 isBatchingUpdates 判斷是直接更新 this.state 還是放到隊列中回頭再說,而 isBatchingUpdates 默認是 false,也就表示 setState 會同步更新 this.state,但是,有一個函數 batchedUpdates,這個函數會把 isBatchingUpdates 修改為 true,而當 React 在調用事件處理函數之前就會調用這個 batchedUpdates,造成的後果,就是由 React 控制的事件處理過程 setState 不會同步更新 this.state。
劃重點時間:
由 React 控制的事件處理過程 setState 不會同步更新 this.state!
也就是說,
在 React 控制之外的情況, setState 會同步更新 this.state!
但大部份的使用情況下,我們都是使用了 React 庫中的表單組件,例如 select、input、button 等等,它們都是 React 庫中人造的組件與事件,是處於 React 庫的控制之下,比如組件原色 onClick 都是經過 React 包裝。在這個情況下,setState 就會以非同步的方式執行。
所以一般來說,其他答主會誤認為 setState 就是非同步執行。
實際上,繞過 React 通過 JavaScript 原生 addEventListener 直接添加的事件處理函數,還有使用 setTimeout/setInterval 等 React 無法掌控的 APIs情況下,就會出現同步更新 state 的情況。
曾經有人突發奇想,把 setState 函數 promise 風格化,全部非同步執行,並返回一個 promise。這是很有意思的,並給 React 提了 PR,裡邊的故事可以參考我寫的文章: 從setState promise化的探討 體會React團隊設計思想
其實,還有很多可以深入的細節。我先吃飯去了,回來有時間可以更多跟大家討論。
這種說法的意思是,因為 setstate 不保證同步更新,所以你加了 increment 之後,在一個消息循環之內,下次取出來可能還是原來的值。
但實際上我經常這麼寫,別人這麼寫我也不攔著。因為大部分的 setstate 都是在 dom 事件響應中執行的,一個消息循環之內一般沒有機會執行多次。當然這麼寫是比較 dirty 的。
其實我對 setstate 這個 api 的設計還是有點意見的,你非同步更新 dom 沒問題啊,但是為啥要非同步更新 state 數據呢?我一直不理解這個。在我看來完全可以同步更新 state,但是非同步刷新界面嘛。
我之前寫過三篇專欄文章,應該能夠回答你的問題:
setState:這個API設計到底怎麼樣
setState為什麼不會同步更新組件狀態
setState何時同步更新狀態
了解更多React相關知識關注我的專欄吧《 進擊的React》,還有記得關注我 @程墨Morgan
看了這個問題的答案之後我有一個感想:
大部分前端在使用 React 時,並不自己知道在幹什麼。
這個問題其實涉及到一個很基礎的問題,那就是「非同步」。
this.setState({
counter: this.state.counter + this.props.increment,
});
由於 this.setState 並不是 this.state = 操作,所以這中間肯定是「有小動作」的。高票答案已經說了小動作是什麼。
在你得知 this.setState 「有可能」會非同步更新 this.state 的請求下,就絕對應該更偏向回調的方式
this.setState((prevState, props) =&> ({
counter: prevState.counter + props.increment
}))
這是一種範式選擇:非同步與回調配合起來更自然更少出錯。
不過 API 不應該這樣設計啊……
要非同步就永遠非同步,要同步就永遠同步,「有可能非同步」是什麼垃圾?(對我在說 React 這個 API 很垃圾)
"May Be Asynchronous"? WTF...
當年 jQ 就是因為 deferred 對象有時同步有時非同步被人罵慘了,你們不能雙標。
留一個坑給新手踩不太好。
這是老生常談的話題,以至於我覺得不應該正面回答,而應該反問:
在(向別人)提出問題之前,已經做了哪些調查?得到了哪些進展?有沒有人遇到同樣的問題?別人有哪些進展?
即使只在知乎搜索"react setState",也是能找到答案的,從示例到源碼解析都有。
問別人之前先問自己,是非常重要的工作習慣——既培養自己解決問題的能力,也節約了它人的時間。
養成這樣的習慣,是工作中遠比某一項知識、技能更重要的東西
====更新=====
@Lucas HC 對setState的解釋很棒,這個問題下面直到現在才出現正確答案是我沒料到的。
但針對【「冷嘲熱諷他人提問,數落他人不會自己查問題解決」是自身戾氣的體現。】的觀點,我仍然認為,在知乎本身就能找到足夠資料的情況下,提重複的問題是一種spam,就像給開源項目提issue之前先搜一下是不是已經有類似問題,是對他人時間的基本尊重。
先不說這個問題是不是老生常談,其實很多人的解釋讓人模糊不定,很多文章就是這樣。由於最近寫了類react框架:215566435/Luy,之後,對於setState的印象還是挺深刻的,那麼就講一講吧。
為什麼非同步?
class C extends React.Component {
constructor(props) {
super(props)
this.state = {
c: 0,
a:0,//之後用
b:0//之後用
}
}
componentDidMount() {
this.setState({
c: this.state.c + 1
})
console.log(this.state.c)//此時輸出還是0
}
render() {
return (
&
&
}
}
這個是一個最常見的場景,在setState以後,馬上獲取state的值,可能是不變的(在settimeout等非同步函數里會變)。這麼做的設計叫做:batching,其實就是批量。
這麼說太抽象了,用幾行代碼說一下
componentDidMount() {
this.setState({
a: this.state.a + 1
})
this.setState({
b: this.state.b + 1
})
this.setState({
c: this.state.c + 1
})
}
假設我們的setState不是非同步的,我們一次點擊三次setState,那麼就代表,我們要渲染三次,然而更新的這三次,其實是可以合成為一次的。
this.setState({
c: this.state.c + 1,
b: this.state.b + 1,
a: this.state.a + 1
})
這樣,無論你在一次componentDidMount事件中執行多少次,我都只渲染一次,那是不是性能就提高了很多?沒錯,我們要明白為什麼setState是非同步之前就必須要明白一個道理:非同步的原因是為了提高性能。
為什麼非同步能提高性能?
還記得之前我說過,「在一次componentDidMount中無論執行多少次setState都會渲染一次」這個道理嗎?那這個跟非同步又有什麼關係呢?
看圖其實很容易就明白,其實setState並不是真正的「非同步「,而是你每次setState的時候,把你要設置的值放進一個隊列里,然後等你的componentDidMount回調執行完畢以後,再調用組件的更新方法進行更新。
那麼這樣的一個流程就能做到:事件回調---&>收集要更新的屬性----&>一次性更新.
因此,在收集屬性的階段,你不可能拿到最新的屬性。
componentDidMount() {
this.setState({
a: this.state.a + 1
})
console.log(this.state.a)//現在處於收集屬性階段,不可能觸發更新,所以state不會變.
}
總結一下
- setState並不會馬上更新,而是會收集屬性,然後再更新。
- 收集屬性的過程中,並不會更新,所以拿到的值永遠是之前的
- 通過收集屬性,收集完畢以後再更新,這麼做顯然能提高很大的性能
- 最後,setState其實並不是真正的傳統性非同步(假非同步),只是「延後」了更新時機,這一點非常重要
事件和生命周期函數的不同
在事件中進行多次setState又會有所不同,只會執行最後一次的操作,拿一個代碼來說
class C extends React.Component {
constructor(props) {
super(props)
this.state = {
c: 0
}
}
click() {
this.setState({
c: this.state.c + 1
})
this.setState({
c: this.state.c + 1
})
this.setState({
c: this.state.c + 1
})
}
render() {
return (
&
{this.state.c}
&
}
}
上述代碼中,只會執行其中一個setState,也就是最後一個,不過很遺憾的是,在這裡的setState依舊是「延後」的,最後你看到的state.c變化是:1
這一點和生命周期函數及其不一樣
componentDidMount() {
this.setState({
c: this.state.c + 1
})
this.setState({
c: this.state.c + 1
})
this.setState({
c: this.state.c + 1
})
}
在這個聲明周期函數中,我們最後更新完後的state會是+3。
React里setState其實有兩個參數
setState(object,callback)
沒錯了,就是callback,這個callback會在組件更新之後,立馬執行調用。所以,在這個callback中,我們可以拿到最新的state.
函數式參數
setState((state)=&>({
c:state.c+1
}))
這種寫法為什麼就能執行正確的更新了呢?其實還是很簡單的。
還記得之前我們說的「收集state」這個過程嗎?實際上,我們在丟進去一個函數的時候,React會幫我們「收集這些函數」,然後在等生命周期或者事件回調執行完畢以後,一次性執行這些回調,然後更新state,相當於把state存在函數里,然後最後更新的時候再從函數里拿出來,他保證了你每次調用的時候,你都會拿到新的state值.
state全部總結
- setState是非同步的,但是是假非同步,是一種「延遲」,而不是真非同步。
- 事件回調和生命周期中,的setState會有所不同,但是依舊遵循「延遲」更新策略。
- 想要獲取state的最新值,請在setState的第二個參數里丟進去一個函數獲取
- state的參數還可以是一個帶有返回值的函數
setState的時機,總結:
- componentDidMount鉤子
- componentWillReceiveProps鉤子
- 事件回調
- setTimeout回調
最後
在我寫完215566435/Luy後,我對React內部理解更加深刻,現在項目很短,只有1100+行,大家有興趣可以去學習,無恥求星啦...(最近一直在更新,這周開始寫文檔!一直在維護
如果你就是一次同步行為應該沒具體區別,因為 React 會在交互一次事務裡面處理這個事,當然你 clone 一份不可變數據再拿來操作,最後同步到視圖應該更可靠。第二種函數只是會在執行事務代碼裡面進行執行,所以是在內部周期準確執行的一份代碼,不受外部影響。如果搞不清楚的情況下,優先使用第二種。
import React from "react";
export default class App extends React.Component {
constructor(props) {
super(props);
this.state = {
counter: 0,
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
// 點擊按鈕後只增加5,我們實際上想讓他增加10
// this.setState({
// counter: this.state.counter + this.props.increment,
// });
// this.setState({
// counter: this.state.counter + this.props.increment,
// });
// 點擊按鈕後能增加10
this.setState((prevState) =&> ({
counter: prevState.counter + this.props.increment,
}));
this.setState((prevState) =&> ({
counter: prevState.counter + this.props.increment,
}));
}
render() {
return (
&
&{this.state.counter}&
&
&