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 這些理念/思路受什麼啓發 ? 怎麼想到的 ? 又怎麼實現的? ...