從零開始的react入門教程(六),一篇文章理解react組件生命周期


壹 ❀ 引

學習任何一門框架,無論是vue、react亦或是angular,我們除了需要熟練掌握框架語法外,了解框架自身的生命周期也是至關重要的。一方面生命周期在面試中多多少少總是會提及,其次了解框架內部運轉過程,對於幫助你寫出符合預期的代碼也是極有幫助。

但對於大多數剛接觸一門框架的初學者而言,以react為例內部就存在數十個生命周期函數,它們分別代表什么含義,應該在何種場景去使用它,這些都是讓人頭痛的問題。

事實上,react中的組件與大自然中的生物類似,就像人有出生,成長與死亡的過程,react組件也有裝載(初始化,類似出生),更新(成長)與卸載(死亡)三個階段。將組件的生命周期按照這三個階段去划分,並理解每個階段應該做什么事,你會發現這樣去理解會清晰很多。

那么本文並不會像其它文章那樣,開局一張生命周期運轉圖。我們將從三個階段分別介紹react組件的生命周期函數作用,以及在開發中你可能使用它的場景,那么本文開始。

貳 ❀ 裝載(Mount)階段

所謂裝載階段,說直白點就是組件的初始化階段。想象一下我們玩一款角色扮演游戲,角色在出生前我們都需要自定義角色性別,定義角色名,更開放一點游戲還會提供捏臉功能,比如選擇發型,發色,眼睛大小,身材比例等等屬性。

react裝載階段也是在做相同的事,組件初始化階段應該渲染什么樣的DOM結構?需要展示哪些屬性提供哪些交互功能?這些都是在裝載階段決定好的;當然,就像人只會出生一次,react的裝載階段也只有一次。react裝載階段會執行的生命周期函數如下,讓我們一一介紹它們:

  • constructor
  • getInitialState
  • getDefaultProps
  • componentWillMount
  • render
  • componentDidMount

貳 ❀ 壹 constructor

在前面文章的例子中,我們已經跟constructor打過招呼了,如果你了解ES6語法,對於constructor一定不會陌生。在ES6中,constructor就是構造方法,簡單復習下ES5的構造函數與ES6的Class類,以下兩段代碼作用相同:

// ES5
function Parent1(name, age) {
    this.name = name;
    this.age = age;
};
Parent1.prototype.sayName = function () {
    console.log(this.name);
};

const son1 = new Parent1('echo', 27);
son1.sayName();// echo 

// ES6
class Parent2 {
    constructor(name,age){
        this.name = name;
        this.age = age;
    }
    sayName() {
        console.log(this.name);
    }
};

const son2 = new Parent2('echo',27);
son2.sayName();// echo

由於ES6的Class類提供了繼承,ES6明確規定,當子類需要繼承父類時,需要在子類的constructor中執行一次super函數,目的就是調用父類的constructor函數,從而繼承到父類constructor中定義的屬性方法。

class Parent {
  constructor() {
    console.log(new.target.name);
  }
}
class Child extends Parent {
  constructor() {
    super();
  }
}
new Parent() // Parent
new Child() // Child

可以看到,當執行new Child()時,雖然在Child內部super()等同於調用Parent的constructor方法,但此時this指向卻不是parent,而是Child的實例。

一個簡單的繼承例子就是:

class Parent {
    constructor(a, b) {
        this.a = a;
        this.b = b;
    }
}
class Child extends Parent {
    constructor(a, b, c) {
        super(a, b);
        this.c = c;
    }
}
const o = new Child(1, 2, 3);
console.log(o.a, o.b, o.c);// 1 2 3

可以看到child的constructor內並未定義a, b屬性,但通過super,我們成功從父類Parent繼承了這兩個屬性。所以本質上來說,此時的super()等同於Parent.prototype.constructor.call(Child,1,2),當然這句代碼無法正確執行,畢竟ES6明確規定Class只能通過new調用,我們的目的也只是解釋了這個過程而已。

OK,扯的有點遠了,讓我們回歸正題。在react的constructor方法中,我們一般做三件事。

  • 確保在constructor中使用this前執行super,否則會報錯,這是ES6的硬性規定。
  • 定義組件內部的state。
  • 為組件內部定義的方法綁定this。

我想這三點大家應該很好理解,我們一一解釋。

第一點,由於react組件也是通過Class繼承聲明,所以如果需要使用constructor,一般推薦調用super,否則可能會報錯。

第二點,我們在前文提到,react組件的數據由外部傳遞的props與內部定義的state共同構成,所以如果我們希望為一個組件定義內部屬性state,那么在constructor中定義就是最佳的做法。

第三點,由於ES6中,類的內部函數與this並不是自動綁定的,所以為了方便函數被調用時能指向正確的this,傳統做法可以在constructor提前為其綁定好this。當然除了這種綁定方式,其實還有其它另外兩種做法,有興趣可參考從零開始的react入門教程(三)中關於 奇怪的事件綁定 部分內容的介紹。

我們來看一個綜合的例子:

function Child(props) {
    return (
        <div className="echo">
            <div>{props.name}</div>
            <button onClick={props.sayName}>點我</button>
        </div>
    )
};

class Parent extends React.PureComponent {
    constructor() {
      	// 在使用this前調用,不然就報錯
        super();
      	// 定義組件內部屬性
        this.state = { name: 'echo' };
        // 為組件內部的方法綁定this
        this.sayName = this.sayName.bind(this);
    }

    sayName() {
        console.log(this.state.name);
    }

    render() {
        return (<Child name={this.state.name} sayName = {this.sayName}/>);
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

需要注意的是並不是所有組件都需要定義constructor方法,比如函數組件,這類組件只是單純接受外部傳遞的props並展示DOM,內部並未提供state,所以不需要constructor也完全沒問題,比如上述例子中的Child組件。

貳 ❀ 貳 getInitialState與getDefaultProps

關於這兩個方法,大家可能在react組件的生命周期圖解中見過,但隨着ES6語法的普及,實際項目中對兩者比較陌生也是很正常的事情。在ES6語法之前,react使用React.createClass來創建組件,所以在內部便提供了這兩個方法。其中getInitialState顧名思義,它內部用於返回初始化this.state的數據,而getDefaultProps則用於返回props的默認初始值。

以下兩段代碼目的相同:

const Parent = React.createClass({
    getInitialState: function () {
        return {
            name: 'echo'
        }
    },
    getDefaultProps: function () {
        return {
            age: 26
        }
    }
})

class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { name: 'echo' };
    }

    static defaultProps = {
        age: 26
    }

    render() {
        return <div>{this.props.age}</div>
    }
}

通過對比發現,ES6寫法上對於默認props與state的定義更為簡潔。且由於React.createClass已接近廢棄,所以這兩個方法大家知道是做什么的就行了,而且ES6語法中我們已經有個更棒的寫法來取代它們。

貳 ❀ 叄 componentWillMount與componentDidMount

這兩個方法根據方法名應該就能理解何時觸發,componentWillMount在constructor之后render之前觸發,而componentDidMount在render之后觸發。但事實上,我們在開發中使用componentWillMount的場景並不多,總結來說,如果你有一些操作需要在componentWillMount中做,你完全可以將其放在constructor中去完成。

而componentDidMount在組件初始化中使用的頻率就非常高了,由於它會在render后調用,所以當它觸發時,組件的裝載狀態已經完成了,也就是說能夠渲染的DOM都已經加載好了,但對於組件而言,很多數據可能都來自網絡請求,所以如果你的組件需要去請求一些網絡數據,componentDidMount就是發起請求的好地方,通過請求拿回數據,並再次利用this.setState修改state,從而觸發render再次渲染,這就是一次合理的組件初始化加載了。

我們來看個例子:

class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { age: 27 };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ age: 18 })
        }, 3000);
    }
    render() {
        return <div>{this.state.age}</div>;
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

這個例子中我們用定時器模擬了網絡請求,在3秒后修改age為18。

貳 ❀ 肆 render

對於Class組件而言,render函數是唯一必需聲明的函數,我們在聲明一個Class組件時可以不提供render之外的其它任意函數,但如果不提供render就直接報錯了,因為對於Class組件,你總是得告訴react你這個組件需要渲染什么內容。

當然,有同學就說了,那確實會存在一個組件什么都不需要渲染的情況啊,一般這種情況我們前面也說了,你可以在render中直接返回一個null,達到什么都不渲染的目的。

class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { age: 27 };
    }
    render() {
        return this.state.name ? <div>{this.props.age}</div> : null
    }
}

OK,到這里我們介紹完了組件裝載階段會調用的生命周期函數,最后看個例子,加強執行先后順序的理解。

class Child extends React.PureComponent {
    constructor() {
        super();
        console.log('子的constructor執行了')
    }
    componentWillMount(){
        console.log('子的componentWillMount執行了');
    }
    componentDidMount() {
        console.log('子的componentDidMount執行了');
    }
    render() {
        console.log('子的render執行了')
        return null;
    }
}
class Parent extends React.PureComponent {
    constructor() {
        super();
        console.log('父的constructor執行了')
    }
    componentWillMount(){
        console.log('父的componentWillMount執行了');
    }
    componentDidMount() {
        console.log('父的componentDidMount執行了');
    }
    render() {
        console.log('父的render執行了')
        return <Child />;
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

可以看到,constructor-componentWillMount-render都是依次執行,且執行到父的render后就緊接着執行子的前三個聲明周期函數,父的componentDidMount是在子的componentDidMount執行完成后才觸發。

這並不難理解,由於父組件渲染的內容其實就是子組件Child,如果Child的componentDidMount不執行完成,父render又怎么知道需要渲染什么內容呢?所以等到Child的componentDidMount跑完,Parent的render才算真正跑完,此時才會調用Parent的componentDidMount方法,到此為止,父子組件的裝載就算全部進行完畢。

叄 ❀ 更新(Update)階段

在我們玩角色扮演類游戲時,當我們創建好了游戲角色,進入游戲后,我們的角色肯定不是一層不變的,隨着不停的練級,通過獎勵升級不同的裝備等等,游戲角色的等級,裝備外形都會對應的發生變化,這對應到react組件中便是更新階段。

react更新階段共會執行如下這些函數,我們同樣一一來解釋:

  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate
  • Render
  • componentDidUpdate

叄 ❀ 壹 componentWillReceiveProps

我們知道,對於react組件而言,無論是組件內部的state變化亦或是props變化,都會觸發組件的render再次渲染。那么這個方法從字面意思上就是當組件接受的props發生變化時便會觸發;注意,如果是組件的state變化並不會觸發此方法。

事實上,與方法名componentWillReceiveProps字面意義不同,當一個父組件內部使用了一個子組件,無論父傳給子的props有沒有變化,只要父組件的render被再次調用(除了父組件裝載時render初次調用),子組件的componentWillReceiveProps都會被調用。你可能在想,這樣性能難道不會很差嗎?當然不會,決定組件要不要渲染的其實是shouldComponentUpdate,這個我們后面會說,我們來看個例子:

class Child extends React.PureComponent {
    componentWillReceiveProps(nextProps){
        console.log('子組件的componentWillReceiveProps執行了');
    }
    render() {
        console.log('子的render執行了')
        return <div>1</div>;
    }
}
class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { age: 27 };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ age: 18 })
        }, 3000);
    }
    render() {
        return <Child/>
    }
}
ReactDOM.render(<Parent />, document.getElementById('root'));

可以看到,Parent雖然調用了Child,但並未傳遞任何屬性過去,由於Parent在三秒后通過定時器修改了state,導致Parent的render被再次觸發,奇怪的事情發生了,Child的componentWillReceiveProps執行了,但由於此時Child內並無任何props或state變化,所以Child的render並未觸發第二次。

這就是我們在上文中解釋的,只要父組件的render被再次觸發(除去父組件裝載過程的render調用),子組件的componentWillReceiveProps其實都會被調用一次。

我們再來看一個正確的例子:

class Child extends React.PureComponent {
    componentWillReceiveProps(nextProps){
        console.log('子組件的componentWillReceiveProps執行了');
    }
    render() {
        console.log('子的render執行了')
        return <div>{this.props.age}</div>;
    }
}
class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { age: 27 };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ age: 18 })
        }, 3000);
    }
    render() {
        return <Child age={this.state.age}/>
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

在這個例子中,我們將Parent的age傳遞給了Child,因為子組件也有裝載過程,所以一開始componentWillReceiveProps並不會觸發;三秒之后,Parent修改了state中的age,此時Parent的render被再次調用,子組件的componentWillReceiveProps也被再次調用(你會發現跟props變沒變沒啥關系),但此次由於props變化了,所以Child的render也被再次觸發。

也正是因為子組件的componentWillReceiveProps觸發跟外面傳遞的props有沒有變化沒關系,所以在實際開發中,我們會在componentWillReceiveProps內部對比新舊props的差異,再決定是否需要更新子組件的state,從而決定是否需要再次渲染子組件。

來看個例子:

class Child extends React.PureComponent {
    constructor(props) {
        super(props);
        this.state = {
            name: ''
        }
    }
    componentWillReceiveProps(nextProps) {
        if (nextProps.name !== this.props.name) {
            console.log(nextProps, this.props);
            console.log('傳入的props發生改變了');
            this.setState({ name: nextProps.name });
        }
    }
    render() {
        console.log('子的render執行了')
        return <div>{this.state.name}</div>;
    }
}
class Parent extends React.PureComponent {
    constructor() {
        super();
        this.state = { name: 'echo' };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ name: '聽風是風' })
        }, 3000);
    }
    render() {
        return <Child name={this.state.name} />
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

可以看到,在componentWillReceiveProps中nextProps就是發生變化之后的props,而this.props就是上一次的props。

叄 ❀ 貳 shouldComponentUpdate

我們在componentWillReceiveProps提到,此方法並不是決定組件是否應該渲染的方法,當然在componentWillReceiveProps中修改state的做法除外。真正決定組件是否應該更新的是shouldComponentUpdate,字面意義上就很清晰不是嗎?

shouldComponentUpdate接受兩個參數nextProps, nextState,並返回一個布爾值,當布爾值為true,就是告訴組件你應該更新了,若返回的是false,組件的render則不會被再次觸發。我們來看個例子:

class Parent extends React.Component {
    constructor() {
        super();
        this.state = { name: 'echo' };
    }
    componentDidMount() {
        setTimeout(() => {
            this.setState({ name: '聽風是風' })
        }, 3000);
    }
    shouldComponentUpdate(nextProps, nextState){
        console.log('shouldComponentUpdate執行了')
        return false;
    }
    render() {
    return <div>{this.state.name}</div>
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

注意,這個例子中我們使用的是React.Component而不是React.PureComponent,這是因為在PureComponent中已經默認在shouldComponentUpdate函數中做了新舊props與state的淺比較,因此如果你在PureComponent中使用shouldComponentUpdate會提示報錯。這里為了體現shouldComponentUpdate作用,我們得修改為React.Component

回到這個例子,你會發現雖然我們修改了state,但由於shouldComponentUpdate返回了false,這就導致了組件render並沒有被觸發第二次。

在實際開發中,如果我們使用的是React.Component,那么便可以通過shouldComponentUpdate函數,對於props與state進行手動的比較,並根據你的需求來決定是返回true還是false,從而控制組件是否應該再次更新。不過我們在前面中也說了,一般我們還是推薦使用

PureComponent取代Component,因為淺比較的操作react自動會幫你做,那么使用shouldComponentUpdate的場景那就真得看你有沒有一個特殊需求了。

叄 ❀ 叄 componentWillUpdate與componentDidUpdate

顧名思義,只有當shouldComponentUpdate返回為true,才會有componentWillUpdate與componentDidUpdate的執行。與裝載階段一樣,componentWillUpdate、componentDidUpdate的執行對於render也是一前一后,來看個例子:

class Parent extends React.Component {
    constructor() {
        super();
        this.state = { name: 'echo' };
    }
    componentDidMount() {
        console.log('componentDidMount被觸發了')
        setTimeout(() => {
            this.setState({ name: '聽風是風' })
        }, 3000);
    }
    shouldComponentUpdate(nextProps, nextState){
        console.log('shouldComponentUpdate執行了')
        return true;
    }
    componentWillUpdate(){
        console.log('componentWillUpdate執行了')
    }
    componentDidUpdate(){
        console.log('componentDidUpdate執行了')
    }
    render() {
        console.log('render被觸發了');
        return <div>{this.state.name}</div>
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

那么來結合裝載階段,以及父子組件的情況,我們來總結下這兩個階段的執行順序

class Child extends React.Component {
    constructor(props) {
        console.log('子組件的constructor被觸發了')
        super(props);
        this.state = {
            name: ''
        }
    }
    componentWillMount(){
        console.log('子組件的componentWillMount執行了');
    }
    componentDidMount() {
        console.log('子組件的componentDidMount被觸發了')
    }
    componentWillReceiveProps(nextProps) {
        console.log('子組件的componentWillReceiveProps被觸發了');
    }
    shouldComponentUpdate(nextProps, nextState) {
        console.log('子組件的shouldComponentUpdate執行了')
        return true;
    }
    componentWillUpdate() {
        console.log('子組件的componentWillUpdate執行了')
    }
    componentDidUpdate() {
        console.log('子組件的componentDidUpdate執行了')
    }
    render() {
        console.log('子組件的render執行了')
        return <div>{this.props.name}</div>;
    }
}
class Parent extends React.Component {
    constructor() {
        console.log('父組件的constructor被觸發了')
        super();
        this.state = { name: 'echo' };
    }
    componentWillMount(){
        console.log('父組件的componentWillMount執行了');
    }
    componentDidMount() {
        console.log('父組件的componentDidMount被觸發了')
        setTimeout(() => {
            this.setState({ name: '聽風是風' })
        }, 3000);
    }
    shouldComponentUpdate(nextProps, nextState) {
        console.log('父組件的shouldComponentUpdate執行了')
        return true;
    }
    componentWillUpdate() {
        console.log('父組件的componentWillUpdate執行了')
    }
    componentDidUpdate() {
        console.log('父組件的componentDidUpdate執行了')
    }
    render() {
        console.log('父組件的render被觸發了');
        return <Child name={this.state.name}/>
    }
}

ReactDOM.render(<Parent />, document.getElementById('root'));

圖中顏色較深的這條是父子組件裝載完成,之后就是父子的更新過程,大家可以自行梳理下整個過程,這里就不多做介紹了。

肆 ❀ 卸載(Unmount)階段

react組件的的卸妝過程就很簡單了,只有一個componentWillUnmount函數,當組件要從DOM上移除時,此函數會被調用,實際開發中可能使用的場景不會很多,一般用於清理你在裝載階段創建的一些可能造成內存泄漏的數據或者方法,比如定時器,事件監聽等等。

伍 ❀ 總

OK,到這里,我們完整介紹了react聲明周期的三個階段,以及各個階段會執行的生命周期函數,我想通過這樣的分析,大家對於不同聲明周期函數的區別以及作用應該有了大致的了解,現在,讓我們再來看react聲明周期圖解,是不是會清晰很多呢?

今天也是從下午三點斷斷續續寫到了現在,整理東西果然還是費時間,元旦假期也就這么結束了!!!多么希望今天是一號!!!那么到這里,本文結束,晚安。

參考

react官方文檔 State & 生命周期

微信讀書 深入淺出React和Redux 2.3 組件的生命周期

React的生命周期

何時使用Component還是PureComponent?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM