聊聊React v16.3的UNSAFE類生命週期

愛擼碼的貓發表於2018-09-14

聊聊React v16.3的UNSAFE類生命週期
不知道小夥伴有沒有注意到,自從react更新到16.3版本後,以前使用的componentWillMountcomponentWillReceivePropscomponentWillUpdate三個生命週期函式都有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元件stateselectedData值。會觸發Parent元件重新渲染,而Parent元件重新渲染會觸發Child元件的componentWillReceiveProps生命週期函式執行。如此就會陷入死迴圈。導致程式崩潰。

所以,React官方把componentWillReceiveProps替換為UNSAFE_componentWillReceiveProps,讓小夥伴在使用這個生命週期的時候注意它會有缺陷,要注意避免,比如上面例子,ChildcomponentWillReceiveProps呼叫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;   };
}
複製程式碼

由於componentWillUpdatecomponentDidUpdate這兩個生命週期函式有一定的時間差(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接收最新的PropsnextProps、上一個stateprevState兩個引數,返回返回一個物件來更新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更好的替換方案就是getSnapshotBeforeUpdategetSnapshotBeforeUpdatecomponentDidUpdate只經過了更新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_componentWillMountUNSAFE_componentWillReceivePropsUNSAFE_componentWillUpdate。 (舊的生命週期名稱和新的別名都可以在此版本中使用。)

  • 未來的16.x版本:為componentWillMountcomponentWillReceivePropscomponentWillUpdate啟用棄用警告。 (舊的生命週期名稱和新的別名都可以在此版本中使用,但舊名稱會記錄DEV模式警告。)

  • 17.0:刪除componentWillMountcomponentWillReceivePropscomponentWillUpdate。 (從現在開始,只有新的“UNSAFE_”生命週期名稱將起作用。)

結論

其實,說了這麼多。就兩點:

1、React意識到componentWillMountcomponentWillReceivePropscomponentWillUpdate這三個生命週期函式有缺陷,比較容易導致崩潰。但是由於舊的專案已經在用以及有些老開發者習慣用這些生命週期函式,於是通過給它加UNSAFE_來提醒用它的人要注意它們的缺陷。

2、React加入了兩個新的生命週期函式getSnapshotBeforeUpdategetDerivedStateFromProps,目的為了即使不使用這三個生命週期函式,也能實現只有這三個生命週期能實現的功能。

ps:本文部分內容借鑑參考文章ReactV16.3即將更改的生命週期

相關文章