摘要:把組件看作狀態機有限狀態機使用來控制本地狀態使用來傳遞狀態前面我們探討了如何映射狀態到上初始渲染那么接下來我們談談時如何同步狀態到上的也就是是如何更新組件的是如何對比出頁面變化最小的部分這篇文章會為你解答這些問題在這之前你已經了解了版本內
React 把組件看作狀態機(有限狀態機), 使用state來控制本地狀態, 使用props來傳遞狀態. 前面我們探討了 React 如何映射狀態到 UI 上(初始渲染), 那么接下來我們談談 React 時如何同步狀態到 UI 上的, 也就是:
React 是如何更新組件的?
React 是如何對比出頁面變化最小的部分?
這篇文章會為你解答這些問題.
在這之前你已經了解了React (15-stable版本)內部的一些基本概念, 包括不同類型的組件實例、mount過程、事務、批量更新的大致過程(還沒有? 不用擔心, 為你準備好了從源碼看組件初始渲染、接著從源碼看組件初始渲染);
準備一個demo, 調試源碼, 以便更好理解;
Keep calm and make a big deal !
React 是如何更新組件的?依靠事務進行批量更新;
一次batch(批量)的生命周期就是從ReactDefaultBatchingStrategy事務perform之前(調用ReactUpdates.batchUpdates)到這個事務的最后一個close方法調用后結束;
事務啟動后, 遇到 setState 則將 partial state 存到組件實例的_pendingStateQueue上, 然后將這個組件存到dirtyComponents 數組中, 等到 ReactDefaultBatchingStrategy事務結束時調用runBatchedUpdates批量更新所有組件;
組件的更新是遞歸的, 三種不同類型的組件都有自己的updateComponent方法來決定自己的組件如何更新, 其中 ReactDOMComponent 會采用diff算法對比子元素中最小的變化, 再批量處理.
這個更新過程像是一套流程, 無論你通過setState(或者replaceState)還是新的props去更新一個組件, 都會起作用.
那么具體是什么?讓我們從這套更新流程的開始部分講起...
調用 setState 之前首先, 開始一次batch的入口是在ReactDefaultBatchingStrategy里, 調用里面的batchedUpdates便可以開啟一次batch:
// 批處理策略 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
那么調用 batchedUpdates 后發生了什么?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.
調用 setState 后發生了什么// 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所做的事情 :
調用此層級組件的一系列生命周期函數, 并且在合適的時機更新props、state、context;
re-render, 與之前 render 的 element 比較, 如果兩者key && element.type 相等, 則進入下一層進行更新; 如果不等, 直接移除重新mount
對于 ReactDOMComponent:
updateComponent所做的事情 :
更新這一層級DOM元素屬性;
更新子元素, 調用 ReactMultiChild 的 updateChildren, 對比前后變化、標記變化類型、存到updates中(diff算法主要部分);
批量處理updates
對于 ReactDOMTextComponent :
上面只是每個組件自己更新的過程, 那么 React 是如何一次性更新所有組件的 ? 答案是遞歸.
觀察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 我們發現 React 每次走到一個組件更新過程的最后部分, 都會有一個判斷 : 如果 nextELement 和 prevElement key 和 type 相等, 就會調用receiveComponent. receiveComponent和updateComponent一樣, 每種組件都有一個, 作用就相當于updateComponent 接受了新 props 的版本. 而這里調用的就是子元素的receiveComponent, 進而進行子元素的更新, 于是就形成了遞歸更新、遞歸diff. 因此, 整個流程就像這樣(矢量圖) :
這種更新完一級、diff完一級再進入下一級的過程保證 React 只遍歷一次組件樹就能完成更新, 但代價就是只要前后 render 出元素的 type 和 key 有一個不同就刪除重造, 因此, React 建議頁面要盡量保持穩定的結構.
React 是如何對比出頁面變化最小的部分?你可能會說 React 用 virtual DOM 表示了頁面結構, 每次更新, React 都會re-render出新的 virtual DOM, 再通過 diff 算法對比出前后變化, 最后批量更新. 沒錯, 很好, 這就是大致過程, 但這里存在著一些隱藏的深層問題值得探討 :
React 是如何用 virtual DOM 表示了頁面結構, 從而使任何頁面變化都能被 diff 出來?
React 是如何 diff 出頁面變化最小的部分?
React 如何表示頁面結構class C extends React.Component { render () { return ("dscsdcsd" console.log(e)}>{this.state.val}) } } // 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 等屬性.
key 代表元素唯一id值, 意味著只要id改變, 就算前后元素種類相同, 元素也肯定不一樣了;
type 代表元素種類, 有 function(空的wrapper)、class(自定義類)、string(具體的DOM元素名稱)類型, 與key一樣, 只要改變, 元素肯定不一樣;
props 是元素的屬性, 任何寫在標簽上的屬性(如className="container")都會被存在這里, 如果這個元素有子元素(包括文本內容), props就會有children屬性, 存儲子元素; children屬性是遞歸插入、遞歸更新的依據;
也就是說, 如果元素唯一標識符或者類別或者屬性有變化, 那么它們re-render后對應的 key、type 和props里面的屬性也會改變, 前后一對比即可找出變化. 綜上來看, React 這么表示頁面結構確實能夠反映前后所有變化.
那么 React 是如何 diff 的?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 數組中然后批量處理.
And that‘s it !React 是一個激動人心的庫, 它給我們帶來了前所未有的開發體驗, 但當我們沉浸在使用 React 快速實現需求的喜悅中時, 有必要去探究兩個問題 : Why and How?
為什么 React 會如此流行, 原因是什么? 組件化、快速、足夠簡單、all in js、容易擴展、生態豐富、社區強大...
React 反映了哪些思想/理念/思路 ? 狀態機、webComponents、virtual DOM、virtual stack、異步渲染、多端渲染、單向數據流、反應式更新、函數式編程...
React 這些理念/思路受什么啟發 ? 怎么想到的 ? 又怎么實現的? ...
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/94338.html
摘要:為了能夠更好的使用這個工具,今天就對它進行一下源碼剖析。它內部的關鍵代碼是在不指定的時候等于,這就意味著的源碼剖析到此結束,謝謝觀看當然如果指定了剖析就還得繼續。好了,源碼剖析到此結束,謝謝觀看 React-Redux是用在連接React和Redux上的。如果你想同時用這兩個框架,那么React-Redux基本就是必須的了。為了能夠更好的使用這個工具,今天就對它進行一下源碼剖析。 Pr...
摘要:目前,前端領域中勢頭正盛,使用者眾多卻少有能夠深入剖析內部實現機制和原理。當發現節點已經不存在,則該節點及其子節點會被完全刪除掉,不會用于進一步的比較。 目前,前端領域中 React 勢頭正盛,使用者眾多卻少有能夠深入剖析內部實現機制和原理。本系列文章希望通過剖析 React 源碼,理解其內部的實現原理,知其然更要知其所以然。 React diff 作為 Virtual DOM 的加速...
摘要:閱讀本期周刊,你將快速入門,開啟甜蜜之旅。然則的原理負責發送以及處理消息,創建消息隊列并不斷從隊列中取出消息交給,則用于保存消息。 showImg(/img/bVCN99?w=900&h=385); 2016 年 8 月,Android 7.0 Nougat(牛軋糖)正式發布,那么問題來了,你 Marshmallow 了么(? -? ?) Cupcake、Donut、Gingerbre...
摘要:大家可以看到是構造函數構造出來的,并且內部有一個對象,這個對象是本文接下來要重點介紹的對象,接下來我們就來一窺究竟吧。在構造函數內部就進行了一步操作,那就是創建了一個對象,并掛載到了上。下一篇文章還是流程相關的內容。這是我的剖析 React 源碼的第二篇文章,如果你沒有閱讀過之前的文章,請務必先閱讀一下 第一篇文章 中提到的一些注意事項,能幫助你更好地閱讀源碼。 文章相關資料 React ...
閱讀 2822·2023-04-26 01:00
閱讀 753·2021-10-11 10:59
閱讀 2981·2019-08-30 11:18
閱讀 2677·2019-08-29 11:18
閱讀 1022·2019-08-28 18:28
閱讀 3014·2019-08-26 18:36
閱讀 2135·2019-08-23 18:16
閱讀 1069·2019-08-23 15:56