componentWillMount
、componentWillReceiveProps
、componentWillUpdate
三個生命週期函式都有eslint報警,讓我們使用UNSAFE_
字首的新的生命週期函式。不禁有疑問“react這是意欲何為啊”?為什麼要加UNSAFE_
字首?
為什麼要對這些生命週期函式報警?
1、componentWillMount
componentWillMount
生命週期發生在首次渲染前,一般使用的小夥伴大多在這裡初始化資料或非同步獲取外部資料賦值。初始化資料,react官方建議放在constructor
裡面。而非同步獲取外部資料,渲染並不會等待資料返回後再去渲染。
案例一:如下是安裝時監聽外部事件排程程式的元件示例
class Example extends React.Component {
state = {
value: ''
};
componentWillMount() {
this.setState({
value: this.props.source.value
});
this.props.source.subscribe(this.handleChange);
}
componentWillUnmount() {
this.props.source.unsubscribe(this.handleChange );
}
handleChange = source => {
this.setState({
value: source.value
});
};
}
複製程式碼
試想一下,假如元件在第一次渲染的時候被中斷,由於元件沒有完成渲染,所以並不會執行componentWillUnmount
生命週期(注:很多人經常認為componentWillMount和componentWillUnmount總是配對,但這並不是一定的。只有呼叫componentDidMount後,React才能保證稍後呼叫componentWillUnmount進行清理
)。因此handleSubscriptionChange
還是會在資料返回成功後被執行,這時候setState
由於元件已經被移除,就會導致記憶體洩漏。所以建議把非同步獲取外部資料寫在componentDidMount
生命週期裡,這樣就能保證componentWillUnmount
生命週期會在元件移除的時候被執行,避免記憶體洩漏的風險。
現在,小夥伴清楚為什麼了要用UNSAFE_componentWillMount
替換componentWillMount
了吧(注意:這裡的UNSAFE並不是指安全性,而是表示使用這些生命週期的程式碼將更有可能在未來的React版本中存在缺陷,特別是一旦啟用了非同步渲染
)
2、componentWillReceiveProps
componentWillReceiveProps
生命週期是在props更新時觸發。一般用於props
引數更新時同步更新state引數。但如果在componentWillReceiveProps
生命週期直接呼叫父元件的某些有呼叫setState
的函式,會導致程式死迴圈。
案例二:如下是子元件componentWillReceiveProps
裡呼叫父元件改變state的函式示例
...
class Parent extends React.Component{
constructor(){
super();
this.state={
list: [],
selectedData: {}
};
}
changeSelectData = selectedData => {
this.setState({
selectedData
});
}
render(){
return (
<Clild list={this.state.list} changeSelectData={this.changeSelectData}/>
);
}
}
...
class Child extends React.Component{
constructor(){
super();
this.state={
list: []
};
}
componentWillReceiveProps(nextProps){
this.setState({
list: nextProps.list
})
nextProps.changeSelectData(nextProps.list[0]); //預設選擇第一個
}
...
}
複製程式碼
如上程式碼,在Child
元件的componentWillReceiveProps
裡直接呼叫Parent
元件的changeSelectData
去更新Parent
元件state
的selectedData
值。會觸發Parent
元件重新渲染,而Parent
元件重新渲染會觸發Child
元件的componentWillReceiveProps
生命週期函式執行。如此就會陷入死迴圈。導致程式崩潰。
所以,React官方把componentWillReceiveProps
替換為UNSAFE_componentWillReceiveProps
,讓小夥伴在使用這個生命週期的時候注意它會有缺陷,要注意避免,比如上面例子,Child
在componentWillReceiveProps
呼叫changeSelectData
時先判斷list
是否有更新再確定是否要呼叫,就可以避免死迴圈。
3、componentWillUpdate
componentWillUpdate
生命週期在檢視更新前觸發。一般用於檢視更新前儲存一些資料方便檢視更新完成後賦值。
案例三:如下是列表載入更新後回到當前滾動條位置的案例
class ScrollingList extends React.Component {
listRef = null;
previousScrollOffset = null;
componentWillUpdate(nextProps, nextState) {
if (this.props.list.length < nextProps.list.length) {
this.previousScrollOffset = this.listRef.scrollHeight - this.listRef.scrollTop;
}
}
componentDidUpdate(prevProps, prevState) {
if (this.previousScrollOffset !== null) {
this.listRef.scrollTop = this.listRef.scrollHeight - this.previousScrollOffset;
this.previousScrollOffset = null;
}
}
render() {
return (
`<div>` {/* ...contents... */}`</div>`
);
}
setListRef = ref => { this.listRef = ref; };
}
複製程式碼
由於componentWillUpdate
和componentDidUpdate
這兩個生命週期函式有一定的時間差(componentWillUpdate
後經過渲染、計算、再更新DOM
元素,最後才呼叫componentDidUpdate
),如果這個時間段內使用者剛好拉伸了瀏覽器高度,那componentWillUpdate
計算的previousScrollOffset
就不準確了。如果在componentWillUpdate
進行setState
操作,會出現多次呼叫只更新一次的問題,把setState放
在componentDidUpdate
,能保證每次更新只呼叫一次。
所以,react官方建議把componentWillUpdate
替換為UNSAFE_componentWillUpdate
。如果真的有以上案例的需求,可以使用16.3新加入的一個周期函式getSnapshotBeforeUpdate
。下面會有具體說明,這裡暫時賣個關子。
有什麼替換方案?
1、getDerivedStateFromProps
getDerivedStateFromProps
是官方在16.3新加入的生命週期函式,props
變化時被呼叫,若是父元件重新渲染,也會被呼叫。它返回新的props
值。
案例四:如下是getDerivedStateFromProps
的使用例項
class Example extends React.Component {
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.name !== prevState.name) {
return {
name: nextProps.name
}
}
}
}
複製程式碼
可以看到,getDerivedStateFromProps
接收最新的Props
值nextProps
、上一個state
值prevState
兩個引數,返回返回一個物件來更新state
,或者返回null
表示不需要更新state
。要注意的是,getDerivedStateFromProps
不能訪問this
,所以如果要跟上一個props
值作比較,只能是把上一個props
值存到state
裡作為映象。到這裡你一定有疑問,為什麼不把上一個props
值傳給getDerivedStateFromProps
?官方給的解析如下:
-
在第一次呼叫
getDerivedStateFromProps
(例項化後)時,prevProps
引數將為null
,需要在訪問prevProps
時新增if-not-null
檢查。 -
沒有將以前的
props
傳遞給這個函式,在未來版本的React中釋放記憶體的一個步驟。 (如果React不需要將先前的道具傳遞給生命週期,那麼它不需要將先前的道具物件保留在記憶體中。)
綜上可知,getDerivedStateFromProps
正是官方新加入的用以替代componentWillReceiveProps
的方案。如果說,你的專案會考慮往後的版本相容,建議改用getDerivedStateFromProps
。
2、getSnapshotBeforeUpdate
getSnapshotBeforeUpdate
是跟getDerivedStateFromProps
一起,在16.3新加入的生命週期函式。觸發的時機在最近的更改被提交到DOM
元素前,使得元件可以在更改之前獲得當前值,此生命週期返回的任意值都會作為第三個引數傳給componentDidUpdate
。一般當我們需要在更新DOM
前需要儲存DOM
當前的狀態時會使用這個生命週期,比較常見是用於DOM
更新前獲取滾動位置,更新後恢復到該滾動位置。比如上面的案例三,componentWillUpdate
更好的替換方案就是getSnapshotBeforeUpdate
,getSnapshotBeforeUpdate
到componentDidUpdate
只經過了更新DOM
這一操作。
案例五:如下為案例三的更好的替換方案
class ScrollingList extends React.Component {
listRef = null;
getSnapshotBeforeUpdate(prevProps, prevState) {
if (prevProps.list.length < this.props.list.length) {
return this.listRef.scrollHeight - this.listRef.scrollTop;
}
return null;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (snapshot !== null) {
this.listRef.scrollTop = this.listRef.scrollHeight - snapshot;
}
}
render() {
return (
`<div>` {/* ...contents... */}`</div>`
);
}
setListRef = ref => { this.listRef = ref; };
}
複製程式碼
最後,關於componentWillMount
的替換方案,官方建議把該生命週期函式的邏輯處理放到componentDidMount
裡面去。
隨著React版本迭代,會否相容UNSAFE類生命週期
React官網上的計劃是:
-
16.3:為不安全生命週期引入別名
UNSAFE_componentWillMount
,UNSAFE_componentWillReceiveProps
和UNSAFE_componentWillUpdate
。 (舊的生命週期名稱和新的別名都可以在此版本中使用。) -
未來的16.x版本:為
componentWillMount
,componentWillReceiveProps
和componentWillUpdate
啟用棄用警告。 (舊的生命週期名稱和新的別名都可以在此版本中使用,但舊名稱會記錄DEV模式警告。) -
17.0:刪除
componentWillMount
,componentWillReceiveProps
和componentWillUpdate
。 (從現在開始,只有新的“UNSAFE_”生命週期名稱將起作用。)
結論
其實,說了這麼多。就兩點:
1、React意識到componentWillMount
、componentWillReceiveProps
和componentWillUpdate
這三個生命週期函式有缺陷,比較容易導致崩潰。但是由於舊的專案已經在用以及有些老開發者習慣用這些生命週期函式,於是通過給它加UNSAFE_
來提醒用它的人要注意它們的缺陷。
2、React加入了兩個新的生命週期函式getSnapshotBeforeUpdate
和getDerivedStateFromProps
,目的為了即使不使用這三個生命週期函式,也能實現只有這三個生命週期能實現的功能。
ps:本文部分內容借鑑參考文章ReactV16.3即將更改的生命週期