雖然常用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
。一層層追蹤後如下圖所示:
圖片中黃色數字標識:步驟順序。藍色框表示裡面剩下的步驟都是在performInitialMount
完成的。
步驟按深度優先方式看。
從圖中,我們能看到React元件週期,最早開始與performInitialMount
這個函式裡。其次是render函式,當執行完performInitialMount
後,跳出環境棧,接著執行componentDidMount
函式。
因此:最後的順序是constructor
-> componentWillMount
-> render
-> componentDidMount
3、論證書中的兩句話。
書中還提到一句話:
render
函式並不做實際的渲染動作,他只是返回一個JSX描述的結構。render
函式被呼叫完之後,componentDidMount
函式並不是會被立刻呼叫。componentDidMount
被呼叫的時候,render
函式返回的東西已經引發了渲染,元件已經被『裝載』到了DOM樹上
3.1 第一句話
最直觀的從console.log
來看
這麼一看。好吧,這是一個
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
打日誌,得到如下:
最外層元件的TopLevelWrapper是個null,_renderedComponent
是元件R
,在下一步:
第二個是列印出來的R元件對應的ReactComponent物件,圖中能看到這個元件的下一個元件是ReactDOMComponent
。最後的列印出來的如下:
在圖1上稍微完善了下,另外一個維度的大致呼叫流程如下圖:
為什麼說大致呼叫流程圖,因為,因為在this.performInitialMount
函式裡有一個遞迴的過程。然後React元件除了ReactCompositeComponent
類之外,還有上面提到的ReactDOMComponent
、ReactEmptyComponent
和另外一個內部類(這個類筆者不知道)。而圖中,我們只是畫了一個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
筆者理解為是一個組合元件,什麼是組合元件呢,自認為就是把ReactDOMComponent
、ReactEmptyComponent
這兩種包含到一起的元件。。。扯遠了,看看ReactCompositeComponent.mountComponent
做了什麼把。他呼叫了一個performInitialMount
方法。
4、那performInitialMount
方法幹了個啥???進去一看!重點來了!!他先看看元件有沒有componentWillMount
呀~有的話就呼叫。然後跳進_renderValidatedComponent
這個函式中去了!。好吧,那我們就去_renderValidatedComponent
這個函式中看看。
5、一看發現,_renderValidatedComponent
這個函式又呼叫了_renderValidatedComponentWithoutOwnerOrContext
方法。
6、那就進去看看把,一看便知,_renderValidatedComponentWithoutOwnerOrContext
呼叫了元件的render
方法。var renderedComponent = inst.render();
,一看,render有個返回值!!!那這個返回值是什麼呢!!!打出來一看,圖3、4:
好吧,這有驗證了第三節開頭說的第一句話
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)。
瞅著樣子,感覺就是和
ReactElement
和ReactCompositeComponent
就是不一樣,感覺親切多了!!廢話少說!
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
最後
感覺很多話說的很重複,但是筆者就是想從不同角度說地更仔細點。文章栗子有限,說的只是大概。如有說不明白的或者說錯的地方,麻煩指出。因為,筆者是一個熱愛學習,追求進步的北京打工的外地人!!!!!!