React 把組件看做狀態機(有限狀態機), 使用state來控制本地狀態, 使用props來傳遞狀態. 前面咱們探討了 React 如何映射狀態到 UI 上(初始渲染), 那麼接下來咱們談談 React 時如何同步狀態到 UI 上的, 也就是:javascript
React 是如何更新組件的?java
React 是如何對比出頁面變化最小的部分?react
這篇文章會爲你解答這些問題.git
你已經瞭解了React (15-stable版本)內部的一些基本概念, 包括不一樣類型的組件實例、mount過程、事務、批量更新的大體過程(尚未? 不用擔憂, 爲你準備好了從源碼看組件初始渲染、接着從源碼看組件初始渲染);github
準備一個demo, 調試源碼, 以便更好理解;web
Keep calm and make a big deal !算法
ReactDefaultBatchingStrategy
事務perform以前(調用ReactUpdates.batchUpdates)到這個事務的最後一個close方法調用後結束;ReactDefaultBatchingStrategy
事務結束時調用runBatchedUpdates
批量更新全部組件;updateComponent
方法來決定本身的組件如何更新, 其中 ReactDOMComponent 會採用diff算法對比子元素中最小的變化, 再批量處理.這個更新過程像是一套流程, 不管你經過setState(或者replaceState)仍是新的props去更新一個組件, 都會起做用.編程
讓咱們從這套更新流程的開始部分講起...數組
首先, 開始一次batch的入口是在ReactDefaultBatchingStrategy
裏, 調用裏面的batchedUpdates
即可以開啓一次batch:app
// 批處理策略
var ReactDefaultBatchingStrategy = {
isBatchingUpdates: false,
batchedUpdates: function(callback, a, b, c, d, e) {
var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 開啓一次batch
if (alreadyBatchingUpdates) {
return callback(a, b, c, d, e);
} else {
// 啓動事務, 將callback放進事務裏執行
return transaction.perform(callback, null, a, b, c, d, e);
}
},
};
複製代碼
在 React 中, 調用batchedUpdates
有不少地方, 與更新流程相關的以下
// ReactMount.js
ReactUpdates.batchedUpdates(
batchedMountComponentIntoNode, // 負責初始渲染
componentInstance,
container,
shouldReuseMarkup,
context,
);
// ReactEventListener.js
dispatchEvent: function(topLevelType, nativeEvent) {
...
try {
ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); // 處理事件
} finally {
TopLevelCallbackBookKeeping.release(bookKeeping);
}
},
複製代碼
第一種狀況, React 在首次渲染組件的時候會調用batchedUpdates
, 而後開始渲染組件. 那麼爲何要在這個時候啓動一次batch呢? 不是由於要批量插入, 由於插入過程是遞歸的, 而是由於組件在渲染的過程當中, 會依順序調用各類生命週期函數, 開發者極可能在生命週期函數中(如componentWillMount
或者componentDidMount
)調用setState
. 所以, 開啓一次batch就是要存儲更新(放入dirtyComponents), 而後在事務結束時批量更新. 這樣以來, 在初始渲染流程中, 任何setState
都會生效, 用戶看到的始終是最新的狀態.
第二種狀況, 若是你在HTML元素上或者組件上綁定了事件, 那麼你有可能在事件的監聽函數中調用setState
, 所以, 一樣爲了存儲更新(放入dirtyComponents), 須要啓動批量更新策略. 在回調函數被調用以前, React事件系統中的dispatchEvent
函數負責事件的分發, 在dispatchEvent
中啓動了事務, 開啓了一次batch, 隨後調用了回調函數. 這樣一來, 在事件的監聽函數中調用的setState
就會生效.
也就是說, 任何可能調用 setState 的地方, 在調用以前, React 都會啓動批量更新策略以提早應對可能的setState
React 調用batchedUpdates
時會傳進去一個函數, batchedUpdates
會啓動ReactDefaultBatchingStrategyTransaction
事務, 這個函數就會被放在事務裏執行:
// ReactDefaultBatchingStrategy.js
var transaction = new ReactDefaultBatchingStrategyTransaction(); // 實例化事務
var ReactDefaultBatchingStrategy = {
...
batchedUpdates: function(callback, a, b, c, d, e) {
...
return transaction.perform(callback, null, a, b, c, d, e); // 將callback放進事務裏執行
...
};
複製代碼
ReactDefaultBatchingStrategyTransaction
這個事務控制了批量策略的生命週期:
// ReactDefaultBatchingStrategy.js
var FLUSH_BATCHED_UPDATES = {
initialize: emptyFunction,
close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), // 批量更新
};
var RESET_BATCHED_UPDATES = {
initialize: emptyFunction,
close: function() {
ReactDefaultBatchingStrategy.isBatchingUpdates = false; // 結束本次batch
},
};
var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];
複製代碼
不管你傳進去的函數是什麼, 不管這個函數後續會作什麼, 都會在執行完後調用上面事務的close方法, 先調用flushBatchedUpdates
批量更新, 再結束本次batch.
// ReactBaseClasses.js :
ReactComponent.prototype.setState = function(partialState, callback) {
this.updater.enqueueSetState(this, partialState);
if (callback) {
this.updater.enqueueCallback(this, callback, 'setState');
}
};
// => ReactUpdateQueue.js:
enqueueSetState: function(publicInstance, partialState) {
// 根據 this.setState 中的 this 拿到內部實例, 也就是組件實例
var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
// 取得組件實例的_pendingStateQueue
var queue =
internalInstance._pendingStateQueue ||
(internalInstance._pendingStateQueue = []);
// 將partial state存到_pendingStateQueue
queue.push(partialState);
// 調用enqueueUpdate
enqueueUpdate(internalInstance);
}
// => ReactUpdate.js:
function enqueueUpdate(component) {
ensureInjected(); // 注入默認策略
// 若是沒有開啓batch(或當前batch已結束)就開啓一次batch再執行, 這一般發生在異步回調中調用 setState // 的狀況
if (!batchingStrategy.isBatchingUpdates) {
batchingStrategy.batchedUpdates(enqueueUpdate, component);
return;
}
// 若是batch已經開啓就存儲更新
dirtyComponents.push(component);
if (component._updateBatchNumber == null) {
component._updateBatchNumber = updateBatchNumber + 1;
}
}
複製代碼
也就是說, 調用 setState 會首先拿到內部組件實例, 而後把要更新的partial state存到其_pendingStateQueue中, 而後標記當前組件爲dirtyComponent
, 存到dirtyComponents
數組中. 而後就接着繼續作下面的事情了, 並無當即更新, 這是由於接下來要執行的代碼裏有可能還會調用 setState, 所以只作存儲處理.
首先, 一個事務在執行的時候(包括initialize、perform、close階段), 任何一階段都有可能調用一系列函數, 而且開啓了另外一些事務. 那麼只有等後續開啓的事務執行完, 以前開啓的事務才繼續執行. 下圖是咱們剛纔所說的第一種狀況, 在初始渲染組件期間 setState 後, React 啓動的各類事務和執行的順序:
從圖中能夠看到, 批量更新是在ReactDefaultBatchingStrategyTransaction
事務的close階段, 在flushBatchedUpdates
函數中啓動了ReactUpdatesFlushTransaction
事務負責批量更新.
咱們接着看flushBatchedUpdates
函數, 在ReactUpdates.js中
var flushBatchedUpdates = function () {
// 啓動批量更新事務
while (dirtyComponents.length || asapEnqueued) {
if (dirtyComponents.length) {
var transaction = ReactUpdatesFlushTransaction.getPooled();
transaction.perform(runBatchedUpdates, null, transaction);
ReactUpdatesFlushTransaction.release(transaction);
}
// 批量處理callback
if (asapEnqueued) {
asapEnqueued = false;
var queue = asapCallbackQueue;
asapCallbackQueue = CallbackQueue.getPooled();
queue.notifyAll();
CallbackQueue.release(queue);
}
}
};
複製代碼
flushBatchedUpdates
啓動了一個更新事務, 這個事務執行了runBatchedUpdates
進行批量更新:
// ReactUpdates.js
function runBatchedUpdates(transaction) {
var len = transaction.dirtyComponentsLength;
// 排序保證父組件優先於子組件更新
dirtyComponents.sort(mountOrderComparator);
// 表明批量更新的次數, 保證每一個組件只更新一次
updateBatchNumber++;
// 遍歷 dirtyComponents
for (var i = 0; i < len; i++) {
var component = dirtyComponents[i];
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
...
// 執行更新
ReactReconciler.performUpdateIfNecessary(
component,
transaction.reconcileTransaction,
updateBatchNumber,
);
...
// 存儲 callback以便後續按順序調用
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
transaction.callbackQueue.enqueue(
callbacks[j],
component.getPublicInstance(),
);
}
}
}
}
複製代碼
前面 setState 後將組件推入了dirtyComponents
, 如今就是要遍歷dirtyComponents
數組進行更新了.
ReactReconciler
會調用組件實例的performUpdateIfNecessary
. 若是接收了props, 就會調用此組件的receiveComponent
, 再在裏面調用updateComponent
更新組件; 若是沒有接受props, 可是有新的要更新的狀態(_pendingStateQueue不爲空)就會直接調用updateComponent
來更新:
// ReactCompositeComponent.js
performUpdateIfNecessary: function (transaction) {
if (this._pendingElement != null) {
ReactReconciler.receiveComponent(this, this._pendingElement, transaction, this._context);
} else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
} else {
this._updateBatchNumber = null;
}
}
複製代碼
接下里就是重頭戲updateComponent
了, 它決定了組件若是更新本身和它的後代們. 須要特別注意的是, React 內部三種不一樣的組件類型, 每種組件都有本身的updateComponent
, 有不一樣的行爲.
對於 ReactCompositeComponent (矢量圖):
updateComponent
所作的事情 :
對於 ReactDOMComponent:
updateComponent
所作的事情 :
updateChildren
, 對比先後變化、標記變化類型、存到updates中(diff算法主要部分);對於 ReactDOMTextComponent :
上面只是每一個組件本身更新的過程, 那麼 React 是如何一次性更新全部組件的 ? 答案是遞歸.
觀察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 咱們發現 React 每次走到一個組件更新過程的最後部分, 都會有一個判斷 : 若是 nextELement 和 prevElement key 和 type 相等, 就會調用receiveComponent
. receiveComponent
和updateComponent
同樣, 每種組件都有一個, 做用就至關於updateComponent 接受了新 props 的版本. 而這裏調用的就是子元素的receiveComponent
, 進而進行子元素的更新, 因而就造成了遞歸更新、遞歸diff. 所以, 整個流程就像這樣(矢量圖) :
這種更新完一級、diff完一級再進入下一級的過程保證 React 只遍歷一次組件樹就能完成更新, 但代價就是隻要先後 render 出元素的 type 和 key 有一個不一樣就刪除重造, 所以, React 建議頁面要儘可能保持穩定的結構.
你可能會說 React 用 virtual DOM 表示了頁面結構, 每次更新, React 都會re-render出新的 virtual DOM, 再經過 diff 算法對比出先後變化, 最後批量更新. 沒錯, 很好, 這就是大體過程, 但這裏存在着一些隱藏的深層問題值得探討 :
class C extends React.Component {
render () {
return (
<div className='container'> "dscsdcsd" <i onClick={(e) => console.log(e)}>{this.state.val}</i> <Children val={this.state.val}/> </div> ) } } // virtual DOM(React element) { $$typeof: Symbol(react.element) key: null props: { // props 表明元素上的全部屬性, 有children屬性, 描述子組件, 一樣是元素 children: [ ""dscsdcsd"", {$$typeof: Symbol(react.element), type: "i", key: null, ref: null, props: {…}, …}, {$$typeof: Symbol(react.element), type: class Children, props: {…}, …} ] className: 'container' } ref: null type: "div" _owner: ReactCompositeComponentWrapper {...} // class C 實例化後的對象 _store: {validated: false} _self: null _source: null } 複製代碼
每一個標籤, 不管是DOM元素仍是自定義組件, 都會有 key、type、props、ref 等屬性.
也就是說, 若是元素惟一標識符或者類別或者屬性有變化, 那麼它們re-render後對應的 key、type 和props裏面的屬性也會改變, 先後一對比便可找出變化. 綜上來看, React 這麼表示頁面結構確實可以反映先後全部變化.
React diff 每次只對同一層級的節點進行比對 :
上圖的數字表示遍歷更新的次序.
從父節點開始, 每一層 diff 包括兩個地方
element diff—— 先後 render 出來的 element 的對比, 這個對比是爲了找出先後節點是否是同一節點, 會對比先後render出來的元素它們的 key 和 type. element diff 包括兩個地方, 組件頂層DOM元素對比和子元素的對比:
組件頂層DOM元素對比 :
// ReactCompositeComponent.js/updateComponent => _updateRenderedComponent
_updateRenderedComponent: function(transaction, context) {
// re-render 出element
var nextRenderedElement = this._renderValidatedComponent();
// 對比先後變化
if (shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement)) {
// 若是 key && type 沒變進行下一級更新
ReactReconciler.receiveComponent(...);
} else {
// 若是變了移除重造
ReactReconciler.unmountComponent(prevComponentInstance, false);
...
var child = this._instantiateReactComponent(...);
var nextMarkup = ReactReconciler.mountComponent(...);
this._replaceNodeWithMarkup(...);
}
}
複製代碼
子元素的對比:
// ReactChildReconciler.js
updateChildren: function(...) {
...
for (name in nextChildren) { // 遍歷 re-render 出的elements
...
if (
prevChild != null &&
shouldUpdateReactComponent(prevElement, nextElement)
) {
// 若是key && type 沒變進行下一級更新
ReactReconciler.receiveComponent(...);
nextChildren[name] = prevChild; // 更新完放入 nextChildren, 注意放入的是組件實例
} else {
// 若是變了則移除重建
if (prevChild) {
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
var nextChildInstance = instantiateReactComponent(nextElement, true);
nextChildren[name] = nextChildInstance;
var nextChildMountImage = ReactReconciler.mountComponent(...);
mountImages.push(nextChildMountImage);
}
}
// 再除掉 prevChildren 裏有, nextChildren 裏沒有的組件
for (name in prevChildren) {
if (
prevChildren.hasOwnProperty(name) &&
!(nextChildren && nextChildren.hasOwnProperty(name))
) {
prevChild = prevChildren[name];
removedNodes[name] = ReactReconciler.getHostNode(prevChild);
ReactReconciler.unmountComponent(prevChild, false);
}
}
},
複製代碼
shouldComponentUpdate 函數:
function shouldUpdateReactComponent(prevElement, nextElement) {
var prevEmpty = prevElement === null || prevElement === false;
var nextEmpty = nextElement === null || nextElement === false;
if (prevEmpty || nextEmpty) {
return prevEmpty === nextEmpty;
}
var prevType = typeof prevElement;
var nextType = typeof nextElement;
// 若是先後變化都是字符串、數字類型的則容許更新
if (prevType === 'string' || prevType === 'number') {
return nextType === 'string' || nextType === 'number';
} else {
// 不然檢查 type && key
return (
nextType === 'object' &&
prevElement.type === nextElement.type &&
prevElement.key === nextElement.key
);
}
}
複製代碼
element diff 檢測 type && key 都沒變時會進入下一級更新, 若是變化則直接移除重造新元素, 而後遍歷同級的下一個.
subtree diff ——組件頂層DOM元素包裹的全部子元素(也就是props.children裏的元素)與以前版本的對比, 這個對比是爲了找出同級全部子節點的變化, 包括移除、新建、同級範圍的移動;
// ReactMultiChild.js
_updateChildren: function(...) {
var prevChildren = this._renderedChildren;
var removedNodes = {};
var mountImages = [];
// 拿到更新後子組件實例
var nextChildren = this._reconcilerUpdateChildren();
...
// 遍歷子組件實例
for (name in nextChildren) {
...
var prevChild = prevChildren && prevChildren[name];
var nextChild = nextChildren[name];
// 由於子組件的更新是在原組件實例上更改的, 所以與以前的組件做引用比較便可判斷
if (prevChild === nextChild) {
// 發生了移動
updates = enqueue(
updates,
this.moveChild(prevChild, lastPlacedNode, nextIndex, lastIndex),
);
lastIndex = Math.max(prevChild._mountIndex, lastIndex);
prevChild._mountIndex = nextIndex;
} else {
...
// 有新的組件
updates = enqueue(
updates,
this._mountChildAtIndex(
nextChild,
mountImages[nextMountIndex],
lastPlacedNode,
nextIndex,
transaction,
context,
),
);
nextMountIndex++;
}
nextIndex++;
lastPlacedNode = ReactReconciler.getHostNode(nextChild);
}
// Remove children that are no longer present.
for (name in removedNodes) {
// removedNodes 記錄了全部的移除節點
if (removedNodes.hasOwnProperty(name)) {
updates = enqueue(
updates,
this._unmountChild(prevChildren[name], removedNodes[name]),
);
}
}
if (updates) {
processQueue(this, updates); // 批量處理
}
this._renderedChildren = nextChildren;
},
複製代碼
React 會將同一層級的變化標記, 如 MOVE_EXISTING、REMOVE_NODE、TEXT_CONTENT、INSERT_MARKUP 等, 統一放到 updates 數組中而後批量處理.
React 是一個激動人心的庫, 它給咱們帶來了史無前例的開發體驗, 但當咱們沉浸在使用 React 快速實現需求的喜悅中時, 有必要去探究兩個問題 : Why and How?
爲何 React 會如此流行, 緣由是什麼? 組件化、快速、足夠簡單、all in js、容易擴展、生態豐富、社區強大...
React 反映了哪些思想/理念/思路 ? 狀態機、webComponents、virtual DOM、virtual stack、異步渲染、多端渲染、單向數據流、反應式更新、函數式編程...
React 這些理念/思路受什麼啓發 ? 怎麼想到的 ? 又怎麼實現的? ...
透過現象看本質, 咱們能得到比應用 React 實現需求更有意義的知識.
未完待續....