React 父組件引發子組件重渲的時候,如何保持子組件的狀態更新不受影響?
使用 React 有一段時間了,讀了幾遍文檔,自己也寫了一個簡單的前端項目,對 React 還算熟悉(也許沒有),但是前前後後想了一個月也並沒有找到這個問題的方法。
場景:點擊按鈕發送 websocket 請求,websocket 伺服器返回數據,點擊按鈕的同時觸發按鈕的禁用倒計時,倒計時以計數的形式顯示。
矛盾:
1 websocket 數據在最外層的父組件接收,作為 state(this.state.wsData),然後傳遞給各個子組件,即使計數按鈕沒有接收任何數據,也會因為最外層父組件的 state 變化導致重渲,除非 shouldComponentUpdate 返回 false。
2 計數按鈕組件的點擊函數會更改它內部的 state,用作倒數計數的值(this.state.counter)。如果設置了 shouldComponentUpdate 返回 false,它的確防止了父組件導致的重渲,但是也禁止了自己的重渲。如果不設置 shouldComponentUpdate 為 false,那麼就會受父組件導致的重渲影響,導致計數函數會運行多次。
實現:
目前我的實現方式是在計數組件的 constructor 裡面加入一個更新開關標識變數 this.shouldupdate = false,計數組件的自更新函數的首行設置 this.shouldupdate = true 打開它,狀態更新的回調函數再設置 false 關閉即 this.setState({counter: n}, () =&> this.shouldupdate = false),然後 shouldComponentUpdate() {return this.shouldupdate}。
問題:
1 這個實現不優雅……
2 這個實現的內部執行順序不明顯(因為我沒有讀過 react 的代碼,可能也讀不懂……),我不能保證或者不能完全理解執行順序是不是正確的,只能寄希望於在給 setState 設置回調之後,子組件的狀態更新的瞬間會執行這個回調……即便如此,因為 websocket 伺服器的推送是受很多條件影響的,不確定它的推送會在何時通知子組件,不知道會不會插入到回調之前,兩個過程交叉起來總有一種不確定性,儘管現在從實現效果上來看是正確的,但代碼不能根據效果做執行保證……
3 這個問題是我在 React 中遇到的最沒有辦法解決的問題。
首先,父子組件的狀態更新會互相影響,這本來就是矛盾的。把所有的 state 都提升到父組件中也無法改變這個局面,甚至讓組件關係更加複雜,因為第一在 state 提升後,想要去影響父組件的 state,子組件就必須作為父組件的一個函數組件,成為父組件的一部分,耦合的太嚴重;第二這個子組件的 state 從邏輯上來講並沒有和其他組件公用,沒有提升的理由;
其次,這種實現需求無法迴避。我分析過這個問題的罪魁禍首到底是不是非同步請求,但非同步請求是前端最基本的構成之一。
請各位精通 React 的前輩指點一下,有什麼好的實現方法,還是我的思路就是錯的?
如果就是想這個Button自己顯示倒計時,但倒計時期間又不想被外界props改變引發重新渲染的話,用上兩層Component就好了,外層Component專門負責在countDown沒有結束的時候讓shouldComponentUpdate返回false,內層的Component根據countDown的state改變驅動只有自己的渲染。
其實,如果覺得外層做得事情足夠通用,可以寫一個HoC,差不多這樣寫:
const sealHoc = (WrappedComponent) =&> {
return class extends WrappedComponent {
constructor() {
super(...arguments);
this.sealed = false;
this.seal = this.seal.bind(this);
this.unseal = this.unseal.bind(this);
}
seal() {
this.sealed = true;
}
unseal() {
this.sealed = false;
}
shouldComponentUpdate() {
return this.sealed;
}
render() { 內層的組件使用外層傳入的onSeal和onUnseal來「封印」和「解封」shouldComponentUpdate,差不多這麼寫。 this.state = {countDown: 0}; startCountDown() { shouldComponentUpdate(nextProps, nextState) { render() { 最後用的就是這個。 不過,我覺得大部分情況真不要這麼操心重複渲染的問題,這樣無疑讓代碼複雜了,只有當性能真的成為問題的時候才去解決它。 關注我吧 @程墨Morgan ,還有我的專欄 《進擊的React》。
return &
}
}
}
class CountDownButton extends React.Component {
constructor() {
super(...arguments);
this.startCountDown = this.startCountDown.bind(this);
}
this.props.onSeal this.props.onSeal();
this.setState((state) =&> ({...state, countDown: 5}));
const countDown = () =&> {
if (this.state.countDown !== 0) {
this.setState(state =&> ({countDown: state.countDown -1}));
setTimeout(countDown, 1000);
} else {
this.props.onUnseal this.props.onUnseal();
}
};
setTimeout(countDown, 1000);
}
return this.state.countDown !== 0;
}
return (
&
);
}
}
export default sealHoc(CountDownButton);
render 函數被調用不等同於重渲染。如果一個父組件發生了改變,在 shouldComponentUpdate 沒有返回 false 的情況下,它的 render 和它的所有子組件的 render 都會被調用一次。返回的虛擬 DOM 用於 diff。不管子組件的 state 和 props 有沒有改變。
所以,組件的 render 應當是一個純函數,不要有副作用。
回到題主的問題,其實你這麼做也不能說不對,因為 redux 也是這麼搞的。。。
https://github.com/reactjs/react-redux/blob/4c2670dc11cc067ef106f6c527e6e8b9d47f8af8/src/components/connectAdvanced.js#L18
計數按鈕不需要維護自己的狀態,它應該是一個展示型組件
https://medium.com/@dan_abramov/smart-and-dumb-components-7ca2f9a7c7d0為什麼 不 拆成 並排的 ?
場景不是很看得懂,websocket 獲取到數據後,負責顯示的組建和 按鈕以及計數器組件之間的關係是如何的。
數據顯示的 和數據獲取的按鈕 應該是在同一個容器里的,但計數器應該和 他們的容器 屬於同級別,而不是 該容器的children。點擊按鈕的同時觸發按鈕的禁用倒計時
計數函數是點擊的時候才調用的,不是渲染的時候,所以我不能理解:
如果不設置 shouldComponentUpdate 為 false,那麼就會受父組件導致的重渲影響,導致計數函數會運行多次
按題主的需求是不會出現這個問題的,題主不妨看一下我的這個 demo
第一步,盡量確保每個組件只用props接收自己用得到的數據。
第二步,使用PureComponent。
嗯,只需要一點點微小的工作。
然後,ComponentShouldUpdate好像會把即將變化為的props和state傳給你……手工寫其實也沒什麼問題才對。大概就是比對一下新舊數據唄~?
最後……不要優化不值得優化的東西啊親,有這時間不如去寫小說……(咦
題主是吧計時器寫在render里了嗎?寫在點擊事件的回調函數問題應該是不會重置state的啊
counter為什麼會和上層組件有父子關係的耦合呢?如果一定要有,那由父組件來管理整個狀態更新也沒什麼不對,像redux,還管著整個應用的state哪。
請用immutablejs 配合componentshouldupdate一起食用。
推薦閱讀:
※React V16 錯誤處理(componentDidCatch 示例)
※【譯】React 16 測試版本
※React + HOC + Redux 極簡指南
TAG:React |