React元件:為什麼呼叫順序是constructor -> willMount -> render -> DidMount

lubyxu發表於2019-03-04

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

前言

書中有一段話,關於元件從初始化到掛載經過的宣告週期:
流程:

  • 1、constructor
  • 2、componentWillMount
  • 3、render
  • 4、componentDidMount
    從流程上來看,發現了以前我的想當然錯了!就是render是在componentDidMount之前呼叫的!!怪不得!每次在componentDidMount裡呼叫非同步的時候,render裡面的object.xxx從報錯!原來在componentDidMount之前,已經render過一次,而這個是後object是個null
    那麼現在,我們從React原始碼上來看看,為什麼是這樣一個順序

1、babel編譯

很簡單的一串程式碼,如下:

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編譯後如下:

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.

這樣,我們就明白了,為什麼首先會呼叫constructor了:constructor並不是React元件的原型函式,而是babel編譯後的一個建構函式。所以當例項化元件的時候,自然會先呼叫ES6中的constructor了。

2、consturcotr之後的順序

元件是如何插入到DOM中呢?先看看ES程式碼:

ReactDOM.render(
    <R />,
    document.getElementById(`example`)
);複製程式碼

呼叫的是ReactDOM.render追蹤原始碼,實際呼叫的是ReactMount.render。一層層追蹤後如下圖所示:

圖1:函式呼叫流程
圖1:函式呼叫流程

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

從圖中,我們能看到React元件週期,最早開始與performInitialMount這個函式裡。其次是render函式,當執行完performInitialMount後,跳出環境棧,接著執行componentDidMount函式。

因此:最後的順序是constructor -> componentWillMount -> render -> componentDidMount

3、論證書中的兩句話。

書中還提到一句話:

  • 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步驟開始說起:
1、在ReactMount中的mountComponentIntoNode裡,呼叫了ReactReconciler.mountComponent方法,方法中的wrapperInstance這個時候,是ReactCompositeComponentWrapper物件。返回一個markup(暫時不說)。那我們看看ReactReconciler.mountComponent這個方法是什麼。
2、在ReactReconciler.mountComponent方法中,呼叫了是internalInstance.mountComponent方法,也就是說我們呼叫了『第1步』中的ReactCompositeComponentWrapper物件的mountComponent方法,那我們再去ReactCompositeComponentWrapper.mountComponent看看。
3、ReactCompositeComponent筆者理解為是一個組合元件,什麼是組合元件呢,自認為就是把ReactDOMComponentReactEmptyComponent這兩種包含到一起的元件。。。扯遠了,看看ReactCompositeComponent.mountComponent做了什麼把。他呼叫了一個performInitialMount方法。
4、那performInitialMount方法幹了個啥???進去一看!重點來了!!他先看看元件有沒有componentWillMount呀~有的話就呼叫。然後跳進_renderValidatedComponent這個函式中去了!。好吧,那我們就去_renderValidatedComponent這個函式中看看。
5、一看發現,_renderValidatedComponent這個函式又呼叫了_renderValidatedComponentWithoutOwnerOrContext方法。
6、那就進去看看把,一看便知,_renderValidatedComponentWithoutOwnerOrContext呼叫了元件的render方法。var renderedComponent = inst.render();,一看,render有個返回值!!!那這個返回值是什麼呢!!!打出來一看,圖3、4:

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

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

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

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

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

8、看過程式碼的人,很清楚了_instantiateReactComponent這個函式的主要作用就是根據不同ReactElement,返回不同型別的ReactComponent。接下來呢,在performInitialMount又執行了

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

好了,我們又開始了『第二步』的流程。但是注意,這個時候,還是在當前這個元件的函式棧中。
9、假設,我們renderedElement返回結構是圖4的結構,這個時候的this._renderedComponent那就是ReactDOMComponent物件。那這時,ReactReconciler.mountComponent裡就會呼叫ReactDOMComponent.mountComponent
10、ReactDOMComponent.mountComponent返回的是一個圖5結構的一樣的東東(後面會叫他markup)。

圖5:markup
圖5:markup

瞅著樣子,感覺就是和ReactElementReactCompositeComponent就是不一樣,感覺親切多了!!廢話少說!

11、既然『第10步』的ReactDOMComponent.mountComponent呼叫完了,我們就返回到『第9步』performInitialMount裡,一看!執行完了,返回就是圖5的markup。那麼,這個函式出棧,回到『第3步』。

12、ReactCompositeComponent裡執行完performInitialMount之後,就會呼叫componentDidMount,看看他有沒有componentDidMount這個函式。如果有的話,就會執行

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

(通篇來看,這個呼叫的頻率還是挺高的,具體做什麼,以後再說)

13、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);這句上。
這也是接下來需要研究的。

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

  • 1、ReactComponent群都是先render完之後, 統一做的componentDidMount
  • 2、 就是上面的,為什麼是先render -> DOM -> componentDidMount

最後

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

相關文章