一段探索React自建內部構造的旅程

一段探索 React 自建內部構造的旅程

在先前的文章里我們涵蓋了React基本原理如何構建更加復雜的交互組件。此篇文章我們將會繼續探索React組件的特性,特別是生命周期。

稍微思考一下React組件所做的事,首先想到的是一點是:React描述了如何去渲染(DOM)。我們已經知道React使用render()方法來達到這個目的。然而僅有render()方法可能不一定都能滿足我們的需求。如果在組件rendered之前或之后我們需要做些額外的事情該怎么做呢?我們需要做些什么以避免重復渲染(re-render)呢?

看起來我們需要對組件(運行)的各個階段進行控制,組件運行所有涉及的各個階段叫做組件的生命周期,并且每一個React組件都會經歷這些階段。React提供了一些方法并在組件處于相應的階段時通知我們。這些方法叫做React組件的生命周期方法且會根據特定并可預測的順序被調用。

基本上所有的React組件的生命周期方法都可以被分割成四個階段:初始化、掛載階段(mounting)更新階段、卸載階段(unmounting)。讓我們來近距離分別研究下各個階段。

初始化階段

初始化階段就是我們分別通過getDefaultProps()getInitialState()方法定義this.props默認值和this.state初始值的階段。

getDefaultProps()方法被調用一次并緩存起來——在多個類實例之間共享。在組件的任何實例被創建之前,我們(的代碼邏輯)不能依賴這里的this.props。這個方法返回一個對象并且屬性如果沒有通過父組件傳入的話相應的屬性會掛載到this.props對象上。

getInitialState()方法也只會被調用一次,(調用時機)剛好是mounting階段開始之前。返回值將會被當成this.state的初始值,且必須是一個對象。

現在我們來證明上面的猜想,實現一個顯示的值可以被增加和減少的組件,基本上就是一個擁有“+”和“-”按鈕的計數器。

var Counter = React.createClass({
    getDefaultProps: function() {
        console.log('getDefaultProps');
        return {
            title: 'Basic counter!!!'
        }
    },

    getInitialState: function() {
        console.log('getInitialState');
        return {
            count: 0
        }
    },

    render: function() {
        console.log('render');
        return (
            <div>
                <h1>{this.props.title}</h1>
                <div>{this.state.count}</div>
                <input type='button' value='+' onClick={this.handleIncrement} />
                <input type='button' value='-' onClick={this.handleDecrement} />
            </div>
        );
    },

    handleIncrement: function() {
        var newCount = this.state.count + 1;
        this.setState({count: newCount});
    },

    handleDecrement: function() {
        var newCount = this.state.count - 1;
        this.setState({count: newCount});
    },

    propTypes: {
        title: React.PropTypes.string
    }
});

ReactDOM.render(
    React.createElement(Counter),
    document.getElementById('app-container')
);

我們通過getDefaultProps()方法配置一個“title”屬性,如果沒有傳入則提供一個默認值。然后通過getInitialState()為組件設置一個初始state值“{count: 0}”。如果運行這段代碼你將會看到控制臺輸出如下結果:

image.png

現在我們想要讓Counter組件可以設置this.state.count初始值和增加/減少的步長值,但依然提供一個默認值:

var Component = React.createClass({
    getDefaultProps: function() {
        console.log('getDefaultProps');
        return {
            title: "Basic counter!!!",
            step: 1
        }
    },

    getInitialState: function() {
        console.log('getInitialState');
        return {
            count: (this.props.initialCount || 0)
        };
    },

    render: function() {
        console.log('render');
        var step = this.props.step;

        return (
            <div>
                <h1>{this.props.title}</h1>
                <div>{this.state.count}</div>
                <input type='button' value='+' onClick={this.updateCounter.bind(this, step)} />
                <input type='button' value='-' onClick={this.updateCounter.bind(this, -step)} />
            </div>
        );
    },

    updateCounter: function(value) {
        var newCount = this.state.count + value;
        this.setState({count: newCount});
    },

    propTypes: {
        title: React.PropTypes.string,
        initialCount: React.PropTypes.number,
        step: React.PropTypes.number
    }
});

ReactDOM.render(
    React.createElement(Component, {initialCount: 5, step: 2}),
    document.getElementById('app-container')
);

這里通過Function.prototype.bind使用偏函數應用(Partial Application)來達到復用代碼的目的。

現在我們擁有了一個可定制化的組件。

增長(Mounting)階段

Mounting階段發生在組件即將被插入到DOM之前。這個階段有兩個方法可以用:componentWillMount()componentDidMount()

componentWillMount()方法是這個階段最先調用的,它只在剛好初始渲染(initial rendering)發生之前被調用一次,也就是React在DOM插入組件之前。需要注意的是在此處調用this.setState()方法將不會觸發重復渲染(re-render)。如果添加下面的代碼到計數器組件我們將會看到此方法在getInitialState()之后且render()之前被調用。

getInitialState: function() {...},
componentWillMount: function() {
    console.log('componentWillMount');
},

componentDidMount()是這個階段第二個被調用的方法,剛好發生在React插入組件到DOM之后,且也只被調用一次?,F在可以更新DOM元素了,這意味著這個方法是初始化其他需要訪問DOM或操作數據的第三方庫的最佳時機。

假設我們想要通過API拉取數據來初始化組件。我們應該直接在計數器組件的componentDidMount()方法拉取數據,但是這讓組件看起來有太多邏輯了,更可取的方案是使用容器組件來做:

var Container = React.createClass({
    getInitialState: function() {
        return {
            data: null,
            fetching: false,
            error: null
        };
    },

    render: function() {
        if (this.props.fetching) {
            return <div>Loading...</div>;
        }

        if (this.props.error) {
            return (
                <div className='error'>
                    {this.state.error.message}
                </div>
            );
        }

        return <Counter {...data} />
    },

    componentDidMount: function() {
        this.setState({fetching: true});

        Axios.get(this.props.url).then(function(res) {
            this.setState({data: res.data, fetching: false});
        }).catch(function(res) {
            this.setState({error: res.data, fetching: false});
        });
    }
});

Axios是一個基于priomise的跨瀏覽器和Node.js的HTTP客戶端。

更新階段

當組件的屬性或者狀態更新時也需要一些方法來供我們執行代碼,這些方法也是組件更新階段的一部分且按照以下的順序被調用:

  1. 當從父組件接收到新的屬性時:
image.png
  1. 當通過this.setState()改變狀態時:
image.png

此階段React組件已經被插入DOM了,因此這些方法將不會在首次render時被調用。

最先被調用的方法是componentWillReceiveProps(),當組件接收到新屬性時被調用。我們可以利用此方法為React組件提供一個在render之前修改state的機會。在此方法內調用this.setState()將不會導致重復render,然后可以通過this.props訪問舊的屬性。例如計數器組件,如果我們想要在任何時候父組件傳入“initialCount”時更新狀態,可以這樣做:

...
componentWillReceiveProps: function(newProps) {
    this.setState({count: newProps.initialCount});
},
...

shouldComponentUpdate()方法允許我們自行決定下一個state更新時是否觸發重復render。此方法返回一個布爾值,且默認是true。但是我們也可以返回false,這樣下面的(生命周期)方法將不會被調用:

  • componentWillUpdate()
  • render()
  • componentDidUpdate()

當有性能瓶頸時也可以使用shouldComponentUpdate()方法(來優化)。尤其是數百個組件一起時重新render的代價將會十分昂貴。為了證明這個猜想我們來看一個例子:

var TextComponent = React.createClass({
    shouldComponentUpdate: function(nextProps, nextState) {
        if (this.props.text === nextProps.text) return false;
        return true;
    },

    render: function() {
        return <textarea value={this.props.text} />;
    }
});

此例中無論何時父組件傳入一個“text”屬性到TextComponent并且text屬性等于當前的“text”屬性時,組件將會不會重復render。

當接收到新的屬性或者state時在render之前會立刻調用componentWillUpdate()方法??梢岳么藭r機來為更新做一些準備工作,雖然這個階段不能調用this.setState()方法:

...
componentWillUpdate: function(nextProps, nextState) {
    console.log('componentWillUpdate', nextProps, nextState);
},
...

componentDidUpdate()方法在React更新DOM之后立刻被調用??梢栽诖朔椒ɡ锊僮鞅桓逻^的DOM或者執行一些后置動作(action)。此方法有兩個參數:

  1. prevProps:舊的屬性
  2. prevState:舊的state

這個方法的一個常見使用場景是當我們使用需要操作更新后的DOM才能工作的第三方庫——如jQuery插件的時候。在componentDidMount()方法內初始化第三方庫,但是在屬性或state更新觸發DOM更新之后也需要同步更新第三方庫來保持接口一致,這些必須在componentDidUpdate()方法內來完成。為了驗證這一點,讓我們看看如何開發一個Select2庫包裹(wrapper)React組件:

var Select2 = React.createClass({
    componentDidMount: function() {
        $(this._ref).select2({data: this.props.items});
    },

    render: function() {
        return (
            <select
                ref={
                    function(input) {
                        this._ref = input;
                    }.bind(this)
                }>
            </select>
        );
    },

    componentDidUpdate: function() {
        $(this._ref).select2('destroy');
        $(this._ref).select2({data: this.props.items});
    }
});

卸載階段(unmounting)

此階段React只提供了一個方法:

  • componentWillUnmount()

它將在組件從DOM卸載之前被調用??梢栽趦炔繄绦腥魏慰赡苄枰那謇砉ぷ?,如無效的計數器或者清理一些在componentDidMount()/componentDidUpdate()內創建的DOM。比如在Select2組件里邊我們可以這樣子:

...
componetWillUnmount: function(){
   $(this._ref).select2('destroy');
},
...

概述

React為我們提供了一種在創建組件時申明一些將會在組件生命周期的特定時機被自動調用的方法的可能。現在我們很清晰的理解了每一個組件生命周期方法所扮演的角色以及他們被調用的順序。這使我們有機會在組件創建和銷毀時執行一些操作。也允許我們在當屬性和狀態變化時做出相應的反應從而更容易的整合第三方庫和追蹤性能問題。

希望您覺得此文對您有用,如果是這樣,請推薦之?。。?/p>

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,908評論 6 541
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,324評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,018評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,675評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,417評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,783評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,779評論 3 446
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,960評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,522評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,267評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,471評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,009評論 5 363
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,698評論 3 348
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,099評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,386評論 1 294
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,204評論 3 398
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,436評論 2 378

推薦閱讀更多精彩內容

  • 原教程內容詳見精益 React 學習指南,這只是我在學習過程中的一些閱讀筆記,個人覺得該教程講解深入淺出,比目前大...
    leonaxiong閱讀 2,851評論 1 18
  • It's a common pattern in React to wrap a component in an ...
    jplyue閱讀 3,291評論 0 2
  • 迪尼是早已名譽世界的冒險家,有關他創造的奇跡傳聞總是一石泛起萬千浪,層出不窮。然而,他失蹤了,不是最近才失蹤的,...
    琦棟閱讀 329評論 1 1
  • (六)修煉之路·其二 每提升到一定等級就有新的劇情、日常任務。剛開始劇情還是能靠自己完成,不過到了一定級別就老是打...
    烈焰陽光閱讀 379評論 16 3