React專題:生命週期

馬蹄疾發表於2018-08-23

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出

來我的 GitHub repo 閱讀完整的專題文章

來我的 個人部落格 獲得無與倫比的閱讀體驗

生命週期,顧名思義,就是從生到死的過程。

而生命週期鉤子,就是從生到死過程中的關鍵節點。

普通人的一生有哪些生命週期鉤子呢?

  • 出生
  • 考上大學
  • 第一份工作
  • 買房
  • 結婚
  • 生子
  • 孩子的生命週期鉤子
  • 退休
  • 臨終遺言

每到關鍵節點,我們總希望有一些沉思時刻,因為這時候做出的決策會改變人生的走向。

React元件也一樣,它會給開發者一些沉思時刻,在這裡,開發者可以改變元件的走向。

非同步渲染下的生命週期

React花了兩年時間祭出Fiber渲染機制。

簡單來說,React將diff的過程叫做Reconciliation。以前這一過程是一氣呵成的,Fiber機制把它改成了非同步。非同步技能將在接下來的版本中逐步解鎖。

明明是一段同步程式碼,怎麼就非同步了呢?

原理是Fiber把任務切成很小的片,每執行一片就把控制權交還給主執行緒,待主執行緒忙完手頭的活再來執行剩下的任務。當然如果某一片的執行時間就很長(比如死迴圈),那就沒主執行緒什麼事了,該崩潰崩潰。

這會給生命週期帶來什麼影響呢?

影響就是掛載和更新之前的生命週期都變的不可靠了。

為什麼這麼講?因為Reconciliation這個過程有可能暫停然後繼續執行,所以掛載和更新之前的生命週期鉤子就有可能不執行或者多次執行,它的表現是不可預期的。

因此16之後的React生命週期迎來了一波大換血,以下生命週期鉤子將被逐漸廢棄:

  • componentWillMount
  • componentWillReceiveProps
  • componentWillUpdate

看出特點了麼,都是帶有will的鉤子。

目前React為這幾個生命週期鉤子提供了別名,分別是:

  • UNSAFE_componentWillMount
  • UNSAFE_componentWillReceiveProps
  • UNSAFE_componentWillUpdate

React17將只提供別名,徹底廢棄這三個大活寶。取這麼個別名意思就是讓你用著噁心。

constructor()

React借用class類的constructor充當初始化鉤子。

React幾乎沒做什麼手腳,但是因為我們只允許通過特定的途徑給元件傳遞引數,所以constructor的引數實際上是被React規定好的。

React規定constructor有三個引數,分別是propscontextupdater

  • props是屬性,它是不可變的。
  • context是全域性上下文。
  • updater是包含一些更新方法的物件,this.setState最終呼叫的是this.updater.enqueueSetState方法,this.forceUpdate最終呼叫的是this.updater.enqueueForceUpdate方法,所以這些API更多是React內部使用,暴露出來是以備開發者不時之需。

在React中,因為所有class元件都要繼承自Component類或者PureComponent類,因此和原生class寫法一樣,要在constructor裡首先呼叫super方法,才能獲得this

constructor生命週期鉤子的最佳實踐是在這裡初始化this.state

當然,你也可以使用屬性初始化器來代替,如下:

import React, { Component } from 'react';

class App extends Component {
    state = {
        name: 'biu',
    };
}

export default App;
複製程式碼

componentWillMount()

?這是React不再推薦使用的API。

這是元件掛載到DOM之前的生命週期鉤子。

很多人會有一個誤區:這個鉤子是請求資料然後將資料插入元素一同掛載的最佳時機。

其實componentWillMount和掛載是同步執行的,意味著執行完這個鉤子,立即掛載。而向伺服器請求資料是非同步執行的。所以無論請求怎麼快,都要排在同步任務之後再處理,這是輩分問題。

也就是說,永遠不可能在這裡將資料插入元素一同掛載。

並不是說不能在這裡請求資料,而是達不到你臆想的效果。

它被廢棄的原因主要有兩點:

  • 本來它就沒什麼用。估計當初是為了成雙成對所以才創造了它吧。
  • 如果它宣告瞭定時器或者訂閱器,在服務端渲染中,componentWillUnmount生命週期鉤子中的清除程式碼不會生效。因為如果元件沒有掛載成功,componentWillUnmount是不會執行的。姚明說的:沒有掛載就沒有解除安裝。
  • 在非同步渲染中,它的表現不穩定。

初始化this.state應該在constructor生命週期鉤子中完成,請求資料應該在componentDidMount生命週期鉤子中完成,所以它不僅被廢棄了,連繼任者都沒有。

static getDerivedStateFromProps(props, state)

?這是React v16.3.0釋出的API。

首先,這是一個靜態方法生命週期鉤子。

也就是說,定義的時候得在方法前加一個static關鍵字,或者直接掛載到class類上。

簡要區分一下例項方法和靜態方法:

  • 例項方法,掛載在this上或者掛載在prototype上,class類不能直接訪問該方法,使用new關鍵字例項化之後,例項可以訪問該方法。
  • 靜態方法,直接掛載在class類上,或者使用新的關鍵字static,例項無法直接訪問該方法。

問題是,為什麼getDerivedStateFromProps生命週期鉤子要設計成靜態方法呢?

這樣開發者就訪問不到this也就是例項了,也就不能在裡面呼叫例項方法或者setsState了。

import React, { Component } from 'react';

class App extends Component {
    render() {
        return (
            <div>React</div>
        );
    }

    static getDerivedStateFromProps(props, state) {}
}

export default App;
複製程式碼

這個生命週期鉤子的使命是根據父元件傳來的props按需更新自己的state,這種state叫做衍生state。返回的物件就是要增量更新的state。

它被設計成靜態方法的目的是保持該方法的純粹,它就是用來定義衍生state的,除此之外不應該在裡面執行任何操作。

這個生命週期鉤子也經歷了一些波折,原本它是被設計成初始化父元件更新接收到props才會觸發,現在只要渲染就會觸發,也就是初始化更新階段都會觸發。

render()

作為一個元件,最核心的功能就是把元素掛載到DOM上,所以render生命週期鉤子是一定會用到的。

render生命週期鉤子怎麼接收模板呢?當然是你return給它。

但是不推薦在return之前寫過多的邏輯,如果邏輯過多,可以封裝成一個函式。

render() {
    // 這裡可以寫一些邏輯
    return (
        <div>
            <input type="text" />
            <button>click</button>
        </div>
    );
}
複製程式碼

注意,千萬不要在render生命週期鉤子裡呼叫this.setState,因為this.setState會引發render,這下就沒完沒了了。主公,有內奸。

componentDidMount()

這是元件掛載到DOM之後的生命週期鉤子。

這可能是除了render之外最重要的生命週期鉤子,因為這時候元件的各方面都準備就緒,天地任你闖。

這就是社會哥,人狠話不多。

componentWillReceiveProps(nextProps)

?這是React不再推薦使用的API。

componentWillReceiveProps生命週期鉤子只有一個引數,更新後的props。

該宣告周期函式可能在兩種情況下被觸發:

  • 元件接收到了新的屬性。
  • 元件沒有收到新的屬性,但是由於父元件重新渲染導致當前元件也被重新渲染。

初始化時並不會觸發該生命週期鉤子。

同樣,因為Fiber機制的引入,這個生命週期鉤子有可能會多次觸發。

shouldComponentUpdate(nextProps, nextState)

這個生命週期鉤子是一個開關,判斷是否需要更新,主要用來優化效能。

有一個例外,如果開發者呼叫this.forceUpdate強制更新,React元件會無視這個鉤子。

shouldComponentUpdate生命週期鉤子預設返回true。也就是說,預設情況下,只要元件觸發了更新,元件就一定會更新。React把判斷的控制權給了開發者。

不過周到的React還提供了一個PureComponent基類,它與Component基類的區別是PureComponent自動實現了一個shouldComponentUpdate生命週期鉤子。

對於元件來說,只有狀態發生改變,才需要重新渲染。所以shouldComponentUpdate生命週期鉤子暴露了兩個引數,開發者可以通過比較this.propsnextPropsthis.statenextState來判斷狀態到底有沒有發生改變,再相應的返回true或false。

什麼情況下狀態沒改變,卻依然觸發了更新呢?舉個例子:

父元件給子元件傳了一個值,當父元件狀態變化,即便子元件接收到的值沒有變化,子元件也會被迫更新。這顯然是非常不合理的,React對此無能為力,只能看開發者的個人造化了。

import React, { Component } from 'react';
import Child from './Child';

class App extends Component {
    state = { name: 'React', star: 1 };

    render() {
        const { name, star } = this.state;
        return (
            <div>
                <Child name={name} />
                <div>{star}</div>
                <button onClick={this.handle}>click</button>
            </div>
        );
    }

	handle = () => {
        this.setState(prevState => ({ star: ++prevState.star }));
    }
}

export default App;
複製程式碼
import React, { Component } from 'react';

class Child extends Component {
    render() {
        return <h1>{this.props.name}</h1>;
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (this.props === nextProps) {
            return false;
        } else {
            return true;
        }
    }
}

export default Child;
複製程式碼

同時要注意引用型別的坑。

下面這種情況,this.propsnextProps永遠不可能相等。

import React, { Component } from 'react';
import Child from './Child';

class App extends Component {
    state = { name: 'React', star: 1 };

    render() {
        return (
            <div>
                <Child name={{ friend: 'Vue' }} />
                <div>{this.state.star}</div>
                <button onClick={this.handle}>click</button>
            </div>
        );
    }

	handle = () => {
        this.setState(prevState => ({ star: ++prevState.star }));
    }
}

export default App;
複製程式碼
import React, { Component } from 'react';

class Child extends Component {
    render() {
        return <h1>{this.props.friend}</h1>;
    }

    shouldComponentUpdate(nextProps, nextState) {
        if (this.props === nextProps) {
            return false;
        } else {
            return true;
        }
    }
}

export default Child;
複製程式碼

解決方法有兩個:

  • 比較this.props.xxxnextProps.xxx
  • 在父元件用一個變數將引用型別快取起來。

所以this.statenextState是隻能用第一種方法比較了,因為React每次更新state都會返回一個新物件,而不是修改原物件。

componentWillUpdate(nextProps, nextState)

?這是React不再推薦使用的API。

shouldComponentUpdate生命週期鉤子返回true,或者呼叫this.forceUpdate之後,會立即執行該生命週期鉤子。

要特別注意,componentWillUpdate生命週期鉤子每次更新前都會執行,所以在這裡呼叫this.setState非常危險,有可能會沒完沒了。

同樣,因為Fiber機制的引入,這個生命週期鉤子有可能會多次呼叫。

getSnapshotBeforeUpdate(prevProps, prevState)

?這是React v16.3.0釋出的API。

顧名思義,儲存狀態快照用的。

它會在元件即將掛載時呼叫,注意,是即將掛載。它甚至呼叫的比render還晚,由此可見render並沒有完成掛載操作,而是進行構建抽象UI的工作。getSnapshotBeforeUpdate執行完就會立即呼叫componentDidUpdate生命週期鉤子。

它是做什麼用的呢?有一些狀態,比如網頁滾動位置,我不需要它持久化,只需要在元件更新以後能夠恢復原來的位置即可。

getSnapshotBeforeUpdate生命週期鉤子返回的值會被componentDidUpdate的第三個引數接收,我們可以利用這個通道儲存一些不需要持久化的狀態,用完即可捨棄。

很顯然,它是用來取代componentWillUpdate生命週期鉤子的。

意思就是說呀,開發者一般用不到它。

componentDidUpdate(nextProps, nextState, snapshot)

這是元件更新之後觸發的生命週期鉤子。

搭配getSnapshotBeforeUpdate生命週期鉤子使用的時候,第三個引數是getSnapshotBeforeUpdate的返回值。

同樣的,componentDidUpdate生命週期鉤子每次更新後都會執行,所以在這裡呼叫this.setState也非常危險,有可能會沒完沒了。

componentWillUnmount()

這是元件解除安裝之前的生命週期鉤子。

為什麼元件快要解除安裝了還需要沉思時刻呢?

因為開發者要擦屁股吖。

React的最佳實踐是,元件中用到的事件監聽器、訂閱器、定時器都要在這裡銷燬。

當然我說的事件監聽器指的是這種:

componentDidMount() {
    document.addEventListener('click', () => {});
}
複製程式碼

因為下面這種React會自動銷燬,不勞煩開發者了。

render(
	return (
    	<button onClick={this.handle}>click</button>
    );
)
複製程式碼

componentDidCatch(error, info)

?這是React v16.3.0釋出的API。

它主要用來捕獲錯誤並進行相應處理,所以它的用法也比較特殊。

定製一個只有componentDidCatch生命週期鉤子的ErrorBoundary元件,它只做一件事:如果捕獲到錯誤,則顯示錯誤提示,如果沒有捕獲到錯誤,則顯示子元件。

將需要捕獲錯誤的元件作為ErrorBoundary的子元件渲染,一旦子元件丟擲錯誤,整個應用依然不會崩潰,而是被ErrorBoundary捕獲。

import React, { Component } from 'react';

class ErrorBoundary extends Component {
    state = { hasError: false };

    render() {
        if (this.state.hasError) {
            return <h1>Something went wrong.</h1>;
        }
        return this.props.children;
    }

    componentDidCatch(error, info) {
        this.setState({ hasError: true });
    }
}

export default ErrorBoundary;
複製程式碼
import React from 'react';
import ErrorBoundary from './ErrorBoundary';
import MyWidget from './MyWidget';

const App = () => {
    return (
        <ErrorBoundary>
            <MyWidget />
        </ErrorBoundary>
    );
}

export default App;
複製程式碼

生命週期

這麼多生命週期鉤子,實際上總結起來只有三個過程:

  • 掛載
  • 更新
  • 解除安裝

掛載和解除安裝只會執行一次,更新會執行多次。

一個完整的React元件生命週期會依次呼叫如下鉤子:

old lifecycle

  • 掛載

    • constructor
    • componentWillMount
    • render
    • componentDidMount
  • 更新

    • componentWillReceiveProps
    • shouldComponentUpdate
    • componentWillUpdate
    • render
    • componentDidUpdate
  • 解除安裝

    • componentWillUnmount

new lifecycle

  • 掛載

    • constructor
    • getDerivedStateFromProps
    • render
    • componentDidMount
  • 更新

    • getDerivedStateFromProps
    • shouldComponentUpdate
    • render
    • getSnapshotBeforeUpdate
    • componentDidUpdate
  • 解除安裝

    • componentWillUnmount

元件樹生命週期呼叫棧

應用初次掛載時,我們以rendercomponentDidMount為例,React首先會呼叫根元件的render鉤子,如果有子元件的話,依次呼叫子元件的render鉤子,呼叫過程其實就是遞迴的順序。

等所有元件的render鉤子都遞迴執行完畢,這時候執行權在最後一個子元件手裡,於是開始觸發下一輪生命週期鉤子,呼叫最後一個子元件的componentDidMount鉤子,然後呼叫棧依次往上遞迴。

lifecycle stack

元件樹的生命週期呼叫棧走的是一個Z字形。

如果根元件沒有定義A生命週期鉤子而子元件定義了,那呼叫棧就從這個子元件的A生命週期鉤子開始。

另外,只要元件內定義了某個生命週期鉤子,即便它沒有任何動作,也會執行。

app.render();
child.render();
grandson.render();
// divide
grandson.componentDidMount();
child.componentDidMount();
app.componentDidMount();
// divide
app.render();
child.render();
grandson.render();
// divide
grandson.componentDidUpdate();
child.componentDidUpdate();
app.componentDidUpdate();
複製程式碼

當然,componentWillMount、componentWillReceiveProps和componentWillUpdate生命週期鉤子有可能被打斷執行,也有可能被多次呼叫,表現是不穩定的。所以React決定逐步廢棄它們。

不過了解整個應用生命週期的正常呼叫順序,還是有助於理解React的。

React專題一覽

什麼是UI

JSX

可變狀態

不可變屬性

生命週期

元件

事件

操作DOM

抽象UI

相關文章