React組件:爲何調用順序是constructor -> willMount -> render -> DidMount

雖然經常使用React、redux編寫SPA,可是這一塊是如何運做,應該如何優化,仍是比較困擾,最近開始閱讀程墨的《深刻淺出React和Redux》,結合以前讀過的React源碼和相關源碼的文章後,打算從源碼的角度,解釋下書中的一些內容。html

前言

書中有一段話,關於組件從初始化到掛載通過的聲明週期:
流程:git

  • 一、constructor
  • 二、componentWillMount
  • 三、render
  • 四、componentDidMount
    從流程上來看,發現了之前個人想固然錯了!就是render是在componentDidMount以前調用的!!怪不得!每次在componentDidMount裏調用異步的時候,render裏面的object.xxx從報錯!原來在componentDidMount以前,已經render過一次,而這個是後object是個null
    那麼如今,咱們從React源碼上來看看,爲何是這樣一個順序

一、babel編譯

很簡單的一串代碼,以下:github

class R extends Component {
  constructor(props) {
    super(props);
    console.log('constructor');
    this.state = {value: props.value}
  }
  componentWillMount() {
    console.log('will mount');
  }
  componentDidMount() {
    console.log('did mount');
  }
  render() {
    console.log('render');
    return (<div>{this.state.value}</div>);
  }
}複製代碼

固然,瀏覽器暫時仍是無法識別ES6的語法的,因此須要經過babel編譯。通過babel編譯後以下:redux

var R = function (_Component) {
  _inherits(R, _Component);

  function R(props) {
    _classCallCheck(this, R);

    var _this = _possibleConstructorReturn(this, (R.__proto__ || Object.getPrototypeOf(R)).call(this, props));

    console.log('constructor');
    _this.state = { value: props.value };
    return _this;
  }

  _createClass(R, [{
    key: 'componentWillMount',
    value: function componentWillMount() {
      console.log('will mount');
    }
  }, {
    key: 'componentDidMount',
    value: function componentDidMount() {
      console.log('did mount');
    }
  }, {
    key: 'render',
    value: function render() {
      console.log('render');
      return React.createElement(
        'div',
        null,
        this.state.value
      );
    }
  }]);

  return R;
}(Component);複製代碼

從上面來看,其實我比較開心且興奮的看到了閉包*原型、繼承,不清楚的小夥伴能夠github.com/mqyqingfeng… 這篇文章補補。
再來看看constructor,從編譯後的代碼來看constructor並非React原型的某個方法,而是babel轉譯後的這個下面的這塊函數。瀏覽器

function R(props) {
    _classCallCheck(this, R);

    var _this = _possibleConstructorReturn(this, (R.__proto__ || Object.getPrototypeOf(R)).call(this, props));

    console.log('constructor');
    _this.state = { value: props.value };
    return _this;
}複製代碼

根據上面複習的原型、繼承, 這是個構造函數。super對應的是_possibleConstructorReturn.bash

這樣,咱們就明白了,爲何首先會調用constructor了:constructor並非React組件的原型函數,而是babel編譯後的一個構造函數。因此當實例化組件的時候,天然會先調用ES6中的constructor了。babel

二、consturcotr以後的順序

組件是如何插入到DOM中呢?先看看ES代碼:閉包

ReactDOM.render(
    <R />,
    document.getElementById('example')
);複製代碼

調用的是ReactDOM.render追蹤源碼,實際調用的是ReactMount.render。一層層追蹤後以下圖所示:app

圖1:函數調用流程
圖1:函數調用流程

圖片中黃色數字標識:步驟順序。藍色框表示裏面剩下的步驟都是在performInitialMount完成的。
步驟按深度優先方式看。異步

從圖中,咱們能看到React組件週期,最先開始與performInitialMount這個函數裏。其次是render函數,當執行完performInitialMount後,跳出環境棧,接着執行componentDidMount函數。

所以:最後的順序是constructor -> componentWillMount -> render -> componentDidMount

三、論證書中的兩句話。

書中還提到一句話:

  • render函數並不作實際的渲染動做,他只是返回一個JSX描述的結構。
  • render函數被調用完以後,componentDidMount函數並非會被馬上調用。componentDidMount被調用的時候,render函數返回的東西已經引起了渲染,組件已經被『裝載』到了DOM樹上

3.1 第一句話

最直觀的從console.log來看

render返回的結構
render返回的結構

這麼一看。好吧,這是一個 ReactElement。原來 render返回的jsx的結構就是個 ReactElement。其實,從babel那翻譯過來的js也能看出, render返回的是一個 ReactElement

{
    key: 'render',
    value: function render() {
      console.log('render');
      return React.createElement(
        'div',
        null,
        this.state.value
  );
}複製代碼

3.2 第二句話

想要解釋第二句話,咱們得更加仔細的分析源碼:

class R extends Component {
  constructor(props) {
    super(props);
    console.log('constructor');
    this.state = {value: props.value}
  }
  componentWillMount() {
    console.log('will mount');
  }
  componentDidMount() {
    console.log('did mount');
  }
  render() {
    console.log('render');
    return (<div>{this.state.value}</div>);
  }
}
ReactDOM.render(
    <R />,
    document.getElementById('example')
);複製代碼

首先,React會在R上面再包裹一層,叫作_topLevelWrapper的層,這個對象建立出來的是一個ReactCompositeComponentWrapper對象,他基於ReactCompositeComponent,以下:

var ReactCompositeComponentWrapper = function (element) {
  this.construct(element);
};
_assign(ReactCompositeComponentWrapper.prototype, ReactCompositeComponent.Mixin, {
  _instantiateReactComponent: instantiateReactComponent
});複製代碼

instantiateReactComponent打日誌,獲得以下:

最外層組件instantiateReactComponent對象
最外層組件instantiateReactComponent對象

最外層組件的TopLevelWrapper是個null,_renderedComponent是組件R,在下一步:

R instantiateReactComponent 的對象
R instantiateReactComponent 的對象

第二個是打印出來的R組件對應的ReactComponent對象,圖中能看到這個組件的下一個組件是ReactDOMComponent。最後的打印出來的以下:

ReactDOMComponent組件
ReactDOMComponent組件

在圖1上稍微完善了下,另一個維度的大體調用流程以下圖:

圖2:函數調用流程
圖2:函數調用流程

爲何說大體調用流程圖,由於,由於在this.performInitialMount函數裏有一個遞歸的過程。而後React組件除了ReactCompositeComponent類以外,還有上面提到的ReactDOMComponentReactEmptyComponent和另一個內部類(這個類筆者不知道)。而圖中,咱們只是畫了一個ReactCompositeComponent

接下來,咱們就說說這個的調用流程把:
ReactMount內部的調用方式,圖上自認爲畫的已是至關清楚了,因此這裏不作詳細說明。
從圖中第3.1步驟開始提及:
一、在ReactMount中的mountComponentIntoNode裏,調用了ReactReconciler.mountComponent方法,方法中的wrapperInstance這個時候,是ReactCompositeComponentWrapper對象。返回一個markup(暫時不說)。那咱們看看ReactReconciler.mountComponent這個方法是什麼。
二、在ReactReconciler.mountComponent方法中,調用了是internalInstance.mountComponent方法,也就是說咱們調用了『第1步』中的ReactCompositeComponentWrapper對象的mountComponent方法,那咱們再去ReactCompositeComponentWrapper.mountComponent看看。
三、ReactCompositeComponent筆者理解爲是一個組合組件,什麼是組合組件呢,自認爲就是把ReactDOMComponentReactEmptyComponent這兩種包含到一塊兒的組件。。。扯遠了,看看ReactCompositeComponent.mountComponent作了什麼把。他調用了一個performInitialMount方法。
四、那performInitialMount方法幹了個啥???進去一看!重點來了!!他先看看組件有沒有componentWillMount呀~有的話就調用。而後跳進_renderValidatedComponent這個函數中去了!。好吧,那咱們就去_renderValidatedComponent這個函數中看看。
五、一看發現,_renderValidatedComponent這個函數又調用了_renderValidatedComponentWithoutOwnerOrContext方法。
六、那就進去看看把,一看便知,_renderValidatedComponentWithoutOwnerOrContext調用了組件的render方法。var renderedComponent = inst.render();,一看,render有個返回值!!!那這個返回值是什麼呢!!!打出來一看,圖三、4:

圖3:TopLevelWrapper的render結構
圖3:TopLevelWrapper的render結構

圖4:R的render結構
圖4:R的render結構

好吧,這有驗證了第三節開頭說的第一句話

render函數並不作實際的渲染動做,他只是返回一個JSX描述的結構(ReactElement)

七、帶着這個ReactElement結構,咱們跳出了_renderValidatedComponentWithoutOwnerOrContext函數,返回了『第5步』後,在_renderValidatedComponent中,繼續返回了這個結構,返回到了第4步中的方法performInitialMount中,執行到_renderValidatedComponent以後。執行下面一句話:

this._renderedComponent = this._instantiateReactComponent(renderedElement);複製代碼

八、看過代碼的人,很清楚了_instantiateReactComponent這個函數的主要做用就是根據不一樣ReactElement,返回不一樣類型的ReactComponent。接下來呢,在performInitialMount又執行了

ReactReconciler.mountComponent(this._renderedComponent, .......);複製代碼

好了,咱們又開始了『第二步』的流程。可是注意,這個時候,仍是在當前這個組件的函數棧中。
九、假設,咱們renderedElement返回結構是圖4的結構,這個時候的this._renderedComponent那就是ReactDOMComponent對象。那這時,ReactReconciler.mountComponent裏就會調用ReactDOMComponent.mountComponent
十、ReactDOMComponent.mountComponent返回的是一個圖5結構的同樣的東東(後面會叫他markup)。

圖5:markup
圖5:markup

瞅着樣子,感受就是和 ReactElementReactCompositeComponent就是不同,感受親切多了!!廢話少說!

十一、既然『第10步』的ReactDOMComponent.mountComponent調用完了,咱們就返回到『第9步』performInitialMount裏,一看!執行完了,返回就是圖5的markup。那麼,這個函數出棧,回到『第3步』。

十二、ReactCompositeComponent裏執行完performInitialMount以後,就會調用componentDidMount,看看他有沒有componentDidMount這個函數。若是有的話,就會執行

transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);複製代碼

(通篇來看,這個調用的頻率仍是挺高的,具體作什麼,之後再說)

1三、ReactCompositeComponent.mountComponent以後,ReactCompositeComponent.mountComponent函數出棧,回到『第一步』:mountComponentIntoNode。返回markup,以後。調用React._mountImageIntoNode函數。這個函數裏,匆匆掃了一下關鍵字:發現,就是將『圖5』的markup結構,轉化成了對應的html結構。裏面有一個筆者認爲的這麼說的點睛之筆是setInnerHTML

綜上所屬:
咱們能夠得出的結論是:

  • constructor -> componentWillMount -> render -> componentDidMount
  • render函數返回的是一個jsx的ReactElement結構。

至於第三個:

render函數被調用完以後,componentDidMount函數並非會被馬上調用。componentDidMount被調用的時候,render函數返回的東西已經引起了渲染,組件已經被『裝載』到了DOM樹上。

筆者還得看看~~ 不過相信,這句話確定是對的!要否則,怎麼會出書,要否則爲何叫作componentDidMount,函數字面意思,就是render以後,先插入DOM,再調用componentDidMount函數。看來,關鍵的地方在transaction.getReactMountReady().enqueue(inst.componentDidMount, inst);這句上。
這也是接下來須要研究的。

限於文章不能寫太長。先暫時這樣,拋出幾個待討論的點,省的忘記。

  • 一、ReactComponent羣都是先render完以後, 統一作的componentDidMount
  • 二、 就是上面的,爲何是先render -> DOM -> componentDidMount

最後

感受不少話說的很重複,可是筆者就是想從不一樣角度說地更仔細點。文章栗子有限,說的只是大概。若有說不明白的或者說錯的地方,麻煩指出。由於,筆者是一個熱愛學習,追求進步的北京打工的外地人!!!!!!

相關文章
相關標籤/搜索