React專題:生命週期

本文是『horseshoe·React專題』系列文章之一,後續會有更多專題推出
來個人 GitHub repo 閱讀完整的專題文章
來個人 我的博客 得到無與倫比的閱讀體驗

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

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

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

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

每到關鍵節點,咱們總但願有一些沉思時刻,由於這時候作出的決策會改變人生的走向。git

React組件也同樣,它會給開發者一些沉思時刻,在這裏,開發者能夠改變組件的走向。github

異步渲染下的生命週期

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

簡單來講,React將diff的過程叫作Reconciliation。之前這一過程是一鼓作氣的,Fiber機制把它改爲了異步。異步技能將在接下來的版本中逐步解鎖。服務器

明明是一段同步代碼,怎麼就異步了呢?app

原理是Fiber把任務切成很小的片,每執行一片就把控制權交還給主線程,待主線程忙完手頭的活再來執行剩下的任務。固然若是某一片的執行時間就很長(好比死循環),那就沒主線程什麼事了,該崩潰崩潰。異步

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

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

爲何這麼講?由於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鉤子,而後調用棧依次往上遞歸。

組件樹的生命週期調用棧走的是一個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

相關文章
相關標籤/搜索