在React中,元件的更新本質上都是由setState操作改變state引起的。因此元件更新的入口在於setState,同樣經過擼原始碼和打斷點分析畫了以下的元件更新的流程圖:
setState的定義在元件mountComponent的時候定義:
inst = new Component(publicProps, publicContext, ReactUpdateQueue);
複製程式碼
function ReactComponent(props, context, updater) {
this.props = props;
this.context = context;
this.refs = emptyObject;
this.updater = updater || ReactNoopUpdateQueue;
}
ReactComponent.prototype.setState = function (partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
複製程式碼
所以setState的真正的定義在 ReactUpdateQueue.js
enqueueSetState: function (publicInstance, partialState) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
if (!internalInstance) {
return;
}
var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
// 將state新增到對應的component的_pendingStateQueue陣列中。
queue.push(partialState);
enqueueUpdate(internalInstance);
}
enqueueCallback: function (publicInstance, callback, callerName) {
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance);
if (!internalInstance) {
return null;
}
// 將callback新增到對應的component的_pendingCallbacks陣列中。
if (internalInstance._pendingCallbacks) {
internalInstance._pendingCallbacks.push(callback);
} else {
internalInstance._pendingCallbacks = [callback];
}
enqueueUpdate(internalInstance);
}
複製程式碼
兩個方法最後都呼叫enqueueUpdate:
function enqueueUpdate(internalInstance) {
ReactUpdates.enqueueUpdate(internalInstance);
}
function enqueueUpdate(component) {
// 確認需要的事務是否注入了。
ensureInjected();
// batchingStrategy.isBatchingUpdates為false的時候,
// 或者說當不處於批量更新的時候,用事務的方式批量的進行component的更新。
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 當處於批量更新階段時,不進行state的更新操作,而是將需要更新的component新增到dirtyComponents陣列中
dirtyComponents.push(component);
}
複製程式碼
這裡需要注意enqueueUpdate中根據batchingStrategy.isBatchingUpdates分別進入不同的流程,當isBatchingUpdates為true的時候表示已經處於批量更新的過程中了,這時候會將所有的有改動的元件push到dirtyComponents中。當isBatchingUpdates為false的時候會執行更新操作,這裡先認為當isBatchingUpdates為false的時候進行的操作是更新元件,實際上的過程是更復雜的,稍後馬上解釋具體的過程。這裡我們先理解下React中的事務的概念,事務的概念根據原始碼中的註釋就可以非常清楚的瞭解了:
* wrappers (injected at creation time)
* + +
* | |
* +-----------------|--------|--------------+
* | v | |
* | +---------------+ | |
* | +--| wrapper1 |---|----+ |
* | | +---------------+ v | |
* | | +-------------+ | |
* | | +----| wrapper2 |--------+ |
* | | | +-------------+ | | |
* | | | | | |
* | v v v v | wrapper
* | +---+ +---+ +---------+ +---+ +---+ | invariants
* perform(anyMethod) | | | | | | | | | | | | maintained
* +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | | | | | | | | | | | |
* | +---+ +---+ +---------+ +---+ +---+ |
* | initialize close |
* +-----------------------------------------+
複製程式碼
簡單來說當使用transaction.perform執行方法method的時候會按順序先執行WRAPPER裡面的initialize方法然後執行method最後再執行close方法。
React提供了基礎的事務物件Transaction,不同的事務的區別就在於initialize和close方法的不同,這個可以通過定義getTransactionWrappers方法來傳入WRAPPER陣列,具體的用法看下原始碼就好了,不過實際使用中是不會要自己去定義事務的,當然要的話也阻止不了~。
回到enqueueUpdate,其呼叫的batchingStrategy.batchedUpdates方法在ReactDefaultBatchingStrategy 中定義了:
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function (callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true;
if (alreadyBatchingUpdates) {
callback(a, b, c, d, e);
} else {
transaction.perform(callback, null, a, b, c, d, e);
}
}
};
複製程式碼
可以看到isBatchingUpdates的初始值是false的,在呼叫batchedUpdates方法的時候會將isBatchingUpdates變數設定為true。然後根據設定之前的isBatchingUpdates的值來執行不同的流程。
對於enqueueUpdate的效果就是,當執行enqueueUpdate的時候如果isBatchingUpdates為true的話(已經處於批量執行操作),則不會進行更新操作,而是將改動的component新增到dirtyComponents陣列中;如果isBatchingUpdates為false的話,會執行batchedUpdates將isBatchingUpdates置為true然後呼叫enqueueUpdate方法,這個時候會用事務的方式來執行enqueueUpdate。
根據流程圖可以知道,事務ReactDefaultBatchingStrategyTransaction的initialize是foo沒有任務操作,接著會執行method即:將改動的元件push到dirtyComponent中,最後執行close方法執行flushBatchedUpdate方法再把isBatchingUpdates重置為false。在flushBatchedUpdates方法中事務執行runBatchedUpdates方法將dirtyComponent中的component依次(先父元件在子元件的順序)進行更新操作。這裡具體的更新的過程看流程圖就可以理解了,需要注意的是updateChildren方法這個方法是virtual DOM的Diff演算法的核心程式碼,作用就是根據更新前後元件的不同進行有效的更新,具體的部分,之後單獨的文章再介紹。
在更新的過程中需要注意的一個方法是_processPendingState方法:
_processPendingState: function (props, context) {
var inst = this._instance;
var queue = this._pendingStateQueue;
var replace = this._pendingReplaceState;
this._pendingReplaceState = false;
this._pendingStateQueue = null;
if (!queue) {
return inst.state;
}
if (replace && queue.length === 1) {
return queue[0];
}
var nextState = _assign({}, replace ? queue[0] : inst.state);
for (var i = replace ? 1 : 0; i < queue.length; i++) {
var partial = queue[i];
_assign(nextState, typeof partial === 'function' ? partial.call(inst, nextState, props, context) : partial);
}
return nextState;
}
複製程式碼
可以看到當setState傳入的是函式的時候,函式被呼叫的時候的傳入的引數是merge了已經遍歷的queue的state的nextState,如果傳入的不是函式則直接merge state至nextState。這也解釋了,為什麼用回撥函式的形式使用setState的時候可以解決state是按照順序最新的state了。
從流程圖可以看到在保證元件更新完畢後會將setState中傳入的callback按照順序依次push到事務的callback queue佇列中,在事務結束的時候close方法中notifyAll就是執行這些callbacks,這樣保證了回撥函式是在元件完全更新完成後執行的,也就是setState的回撥函式傳入的state是更新後的state的原因。
在瞭解了以上的元件更新的流程後,可以看一個場景,栗子如下:
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
import React, { Component } from 'react';
import Hello from './Hello';
class App extends Component {
constructor(props) {
super(props);
this.state = {
appText: 'hello App',
helloText: 'heiheihei'
};
}
handleAppClick = () => {
console.log('App is clicked ~');
this.setState({
appText: 'App is clicked ~'
});
}
render() {
const { appText, helloText } = this.state;
console.log('render App');
return (
<div className="app-container">
<div
onClick={this.handleAppClick}
>{appText}</div>
<Hello
text={helloText}
handleAppClick={this.handleAppClick}
/>
</div>
);
}
}
export default App;
import React, { Component } from 'react';
class Hello extends Component {
constructor(props) {
super(props);
this.state = {
text: 'hello Hello'
};
}
componentWillReceiveProps(nextProps) {
this.setState({
text: nextProps.text + '~'
});
}
handleClick = () => {
this.setState({
text: 'Hello is clicked ~'
});
this.props.handleAppClick();
}
render() {
const { text } = this.state;
console.log('render Hello');
return (
<div>
<div
onClick={this.handleClick}
style={{ color: '#e00' }}
>{text}</div>
</div>
);
}
}
export default Hello;
複製程式碼
點選
hello Hello
複製程式碼
後元件的渲染如下,可以看到父元件到子元件按順序更新了一次:
render App
render Hello
複製程式碼
而不是:
render Hello
render App
render Hello
複製程式碼
批量更新的時候元件的順序由:
dirtyComponents.sort(mountOrderComparator);
複製程式碼
處理的。
到這裡你需要知道這個結果產生的原因在於不是隻有setState的呼叫棧會改變isBatchingUpdates的值
回顧《React事件機制》的流程圖可以知道事件的統一回撥函式dispatchEvent呼叫了ReactUpdates.batchedUpdates用事務的方式進行事件的處理,也就是說點選事件的處理本身就是在一個大的事務中,在setState執行的時候isBatchingUpdates已經是true了,setState做的就是將更新都統一push到dirtyComponents陣列中,在事務結束的時候按照上述的流程進行批量更新,然後將批量執行關閉結束事務。
事務的機制在這裡也保證了React更新的效率,此外在更新元件的時候的virtual DOM的Diff演算法也起到很大的作用,這個在後續的文章再介紹。
參考資料