日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

从源码全面剖析 React 组件更新机制

發(fā)布時間:2025/5/22 编程问答 54 豆豆
生活随笔 收集整理的這篇文章主要介紹了 从源码全面剖析 React 组件更新机制 小編覺得挺不錯的,現(xiàn)在分享給大家,幫大家做個參考.

React 把組件看作狀態(tài)機(有限狀態(tài)機), 使用state來控制本地狀態(tài), 使用props來傳遞狀態(tài). 前面我們探討了 React 如何映射狀態(tài)到 UI 上(初始渲染), 那么接下來我們談?wù)?React 時如何同步狀態(tài)到 UI 上的, 也就是:

React 是如何更新組件的?

React 是如何對比出頁面變化最小的部分?

這篇文章會為你解答這些問題.

在這之前

你已經(jīng)了解了React (15-stable版本)內(nèi)部的一些基本概念, 包括不同類型的組件實例、mount過程、事務(wù)、批量更新的大致過程(還沒有? 不用擔(dān)心, 為你準備好了從源碼看組件初始渲染、接著從源碼看組件初始渲染);

準備一個demo, 調(diào)試源碼, 以便更好理解;

Keep calm and make a big deal !

React 是如何更新組件的?

TL;DR
  • 依靠事務(wù)進行批量更新;
  • 一次batch(批量)的生命周期就是從ReactDefaultBatchingStrategy事務(wù)perform之前(調(diào)用ReactUpdates.batchUpdates)到這個事務(wù)的最后一個close方法調(diào)用后結(jié)束;
  • 事務(wù)啟動后, 遇到 setState 則將 partial state 存到組件實例的_pendingStateQueue上, 然后將這個組件存到dirtyComponents 數(shù)組中, 等到 ReactDefaultBatchingStrategy事務(wù)結(jié)束時調(diào)用runBatchedUpdates批量更新所有組件;
  • 組件的更新是遞歸的, 三種不同類型的組件都有自己的updateComponent方法來決定自己的組件如何更新, 其中 ReactDOMComponent 會采用diff算法對比子元素中最小的變化, 再批量處理.

這個更新過程像是一套流程, 無論你通過setState(或者replaceState)還是新的props去更新一個組件, 都會起作用.

那么具體是什么?

讓我們從這套更新流程的開始部分講起...

調(diào)用 setState 之前

首先, 開始一次batch的入口是在ReactDefaultBatchingStrategy里, 調(diào)用里面的batchedUpdates便可以開啟一次batch:

// 批處理策略 var ReactDefaultBatchingStrategy = {isBatchingUpdates: false, batchedUpdates: function(callback, a, b, c, d, e) {var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;ReactDefaultBatchingStrategy.isBatchingUpdates = true; // 開啟一次batchif (alreadyBatchingUpdates) {return callback(a, b, c, d, e);} else {// 啟動事務(wù), 將callback放進事務(wù)里執(zhí)行return transaction.perform(callback, null, a, b, c, d, e); }}, };

在 React 中, 調(diào)用batchedUpdates有很多地方, 與更新流程相關(guān)的如下

// ReactMount.js ReactUpdates.batchedUpdates(batchedMountComponentIntoNode, // 負責(zé)初始渲染componentInstance,container,shouldReuseMarkup,context, );// ReactEventListener.js dispatchEvent: function(topLevelType, nativeEvent) {...try {ReactUpdates.batchedUpdates(handleTopLevelImpl, bookKeeping); // 處理事件} finally {TopLevelCallbackBookKeeping.release(bookKeeping);} },

第一種情況, React 在首次渲染組件的時候會調(diào)用batchedUpdates, 然后開始渲染組件. 那么為什么要在這個時候啟動一次batch呢? 不是因為要批量插入, 因為插入過程是遞歸的, 而是因為組件在渲染的過程中, 會依順序調(diào)用各種生命周期函數(shù), 開發(fā)者很可能在生命周期函數(shù)中(如componentWillMount或者componentDidMount)調(diào)用setState. 因此, 開啟一次batch就是要存儲更新(放入dirtyComponents), 然后在事務(wù)結(jié)束時批量更新. 這樣以來, 在初始渲染流程中, 任何setState都會生效, 用戶看到的始終是最新的狀態(tài).

第二種情況, 如果你在HTML元素上或者組件上綁定了事件, 那么你有可能在事件的監(jiān)聽函數(shù)中調(diào)用setState, 因此, 同樣為了存儲更新(放入dirtyComponents), 需要啟動批量更新策略. 在回調(diào)函數(shù)被調(diào)用之前, React事件系統(tǒng)中的dispatchEvent函數(shù)負責(zé)事件的分發(fā), 在dispatchEvent中啟動了事務(wù), 開啟了一次batch, 隨后調(diào)用了回調(diào)函數(shù). 這樣一來, 在事件的監(jiān)聽函數(shù)中調(diào)用的setState就會生效.

也就是說, 任何可能調(diào)用 setState 的地方, 在調(diào)用之前, React 都會啟動批量更新策略以提前應(yīng)對可能的setState

那么調(diào)用 batchedUpdates 后發(fā)生了什么?

React 調(diào)用batchedUpdates時會傳進去一個函數(shù), batchedUpdates會啟動ReactDefaultBatchingStrategyTransaction事務(wù), 這個函數(shù)就會被放在事務(wù)里執(zhí)行:

// ReactDefaultBatchingStrategy.js var transaction = new ReactDefaultBatchingStrategyTransaction(); // 實例化事務(wù) var ReactDefaultBatchingStrategy = {...batchedUpdates: function(callback, a, b, c, d, e) {...return transaction.perform(callback, null, a, b, c, d, e); // 將callback放進事務(wù)里執(zhí)行... };

ReactDefaultBatchingStrategyTransaction這個事務(wù)控制了批量策略的生命周期:

// ReactDefaultBatchingStrategy.js var FLUSH_BATCHED_UPDATES = {initialize: emptyFunction,close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates), // 批量更新 }; var RESET_BATCHED_UPDATES = {initialize: emptyFunction,close: function() {ReactDefaultBatchingStrategy.isBatchingUpdates = false; // 結(jié)束本次batch}, }; var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

無論你傳進去的函數(shù)是什么, 無論這個函數(shù)后續(xù)會做什么, 都會在執(zhí)行完后調(diào)用上面事務(wù)的close方法, 先調(diào)用flushBatchedUpdates批量更新, 再結(jié)束本次batch.

調(diào)用 setState 后發(fā)生了什么

// 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) {// 根據(jù) this.setState 中的 this 拿到內(nèi)部實例, 也就是組件實例var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');// 取得組件實例的_pendingStateQueuevar queue =internalInstance._pendingStateQueue ||(internalInstance._pendingStateQueue = []);// 將partial state存到_pendingStateQueuequeue.push(partialState);// 調(diào)用enqueueUpdateenqueueUpdate(internalInstance);}// => ReactUpdate.js: function enqueueUpdate(component) {ensureInjected(); // 注入默認策略// 如果沒有開啟batch(或當(dāng)前batch已結(jié)束)就開啟一次batch再執(zhí)行, 這通常發(fā)生在異步回調(diào)中調(diào)用 setState // 的情況if (!batchingStrategy.isBatchingUpdates) {batchingStrategy.batchedUpdates(enqueueUpdate, component);return;}// 如果batch已經(jīng)開啟就存儲更新dirtyComponents.push(component);if (component._updateBatchNumber == null) {component._updateBatchNumber = updateBatchNumber + 1;} }

也就是說, 調(diào)用 setState 會首先拿到內(nèi)部組件實例, 然后把要更新的partial state存到其_pendingStateQueue中, 然后標記當(dāng)前組件為dirtyComponent, 存到dirtyComponents數(shù)組中. 然后就接著繼續(xù)做下面的事情了, 并沒有立即更新, 這是因為接下來要執(zhí)行的代碼里有可能還會調(diào)用 setState, 因此只做存儲處理.

什么時候批量更新?

首先, 一個事務(wù)在執(zhí)行的時候(包括initialize、perform、close階段), 任何一階段都有可能調(diào)用一系列函數(shù), 并且開啟了另一些事務(wù). 那么只有等后續(xù)開啟的事務(wù)執(zhí)行完, 之前開啟的事務(wù)才繼續(xù)執(zhí)行. 下圖是我們剛才所說的第一種情況, 在初始渲染組件期間 setState 后, React 啟動的各種事務(wù)和執(zhí)行的順序:

從圖中可以看到, 批量更新是在ReactDefaultBatchingStrategyTransaction事務(wù)的close階段, 在flushBatchedUpdates函數(shù)中啟動了ReactUpdatesFlushTransaction事務(wù)負責(zé)批量更新.

怎么批量更新的?

開啟批量更新事務(wù)、批量處理callback

我們接著看flushBatchedUpdates函數(shù), 在ReactUpdates.js中

var flushBatchedUpdates = function () {// 啟動批量更新事務(wù)while (dirtyComponents.length || asapEnqueued) {if (dirtyComponents.length) {var transaction = ReactUpdatesFlushTransaction.getPooled();transaction.perform(runBatchedUpdates, null, transaction);ReactUpdatesFlushTransaction.release(transaction);} // 批量處理callbackif (asapEnqueued) {asapEnqueued = false;var queue = asapCallbackQueue;asapCallbackQueue = CallbackQueue.getPooled();queue.notifyAll();CallbackQueue.release(queue);}} };
遍歷dirtyComponents

flushBatchedUpdates啟動了一個更新事務(wù), 這個事務(wù)執(zhí)行了runBatchedUpdates進行批量更新:

// ReactUpdates.js function runBatchedUpdates(transaction) {var len = transaction.dirtyComponentsLength;// 排序保證父組件優(yōu)先于子組件更新dirtyComponents.sort(mountOrderComparator);// 代表批量更新的次數(shù), 保證每個組件只更新一次updateBatchNumber++;// 遍歷 dirtyComponentsfor (var i = 0; i < len; i++) {var component = dirtyComponents[i];var callbacks = component._pendingCallbacks;component._pendingCallbacks = null;...// 執(zhí)行更新ReactReconciler.performUpdateIfNecessary(component,transaction.reconcileTransaction,updateBatchNumber,);...// 存儲 callback以便后續(xù)按順序調(diào)用if (callbacks) {for (var j = 0; j < callbacks.length; j++) {transaction.callbackQueue.enqueue(callbacks[j],component.getPublicInstance(),);}}} }

前面 setState 后將組件推入了dirtyComponents, 現(xiàn)在就是要遍歷dirtyComponents數(shù)組進行更新了.

根據(jù)不同情況執(zhí)行更新

ReactReconciler會調(diào)用組件實例的performUpdateIfNecessary. 如果接收了props, 就會調(diào)用此組件的receiveComponent, 再在里面調(diào)用updateComponent更新組件; 如果沒有接受props, 但是有新的要更新的狀態(tài)(_pendingStateQueue不為空)就會直接調(diào)用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;} }
調(diào)用組件實例的updateComponent

接下里就是重頭戲updateComponent了, 它決定了組件如果更新自己和它的后代們. 需要特別注意的是, React 內(nèi)部三種不同的組件類型, 每種組件都有自己的updateComponent, 有不同的行為.

對于 ReactCompositeComponent (矢量圖):

updateComponent所做的事情 :

  • 調(diào)用此層級組件的一系列生命周期函數(shù), 并且在合適的時機更新props、state、context;
  • re-render, 與之前 render 的 element 比較, 如果兩者key && element.type 相等, 則進入下一層進行更新; 如果不等, 直接移除重新mount

對于 ReactDOMComponent:

updateComponent所做的事情 :

  • 更新這一層級DOM元素屬性;
  • 更新子元素, 調(diào)用 ReactMultiChild 的 updateChildren, 對比前后變化、標記變化類型、存到updates中(diff算法主要部分);
  • 批量處理updates

對于 ReactDOMTextComponent :

上面只是每個組件自己更新的過程, 那么 React 是如何一次性更新所有組件的 ? 答案是遞歸.

遞歸調(diào)用組件的updateComponent

觀察 ReactCompositeComponent 和 ReactDOMComponent 的更新流程, 我們發(fā)現(xiàn) React 每次走到一個組件更新過程的最后部分, 都會有一個判斷 : 如果 nextELement 和 prevElement key 和 type 相等, 就會調(diào)用receiveComponent. receiveComponent和updateComponent一樣, 每種組件都有一個, 作用就相當(dāng)于updateComponent 接受了新 props 的版本. 而這里調(diào)用的就是子元素的receiveComponent, 進而進行子元素的更新, 于是就形成了遞歸更新、遞歸diff. 因此, 整個流程就像這樣(矢量圖) :

這種更新完一級、diff完一級再進入下一級的過程保證 React 只遍歷一次組件樹就能完成更新, 但代價就是只要前后 render 出元素的 type 和 key 有一個不同就刪除重造, 因此, React 建議頁面要盡量保持穩(wěn)定的結(jié)構(gòu).

React 是如何對比出頁面變化最小的部分?

你可能會說 React 用 virtual DOM 表示了頁面結(jié)構(gòu), 每次更新, React 都會re-render出新的 virtual DOM, 再通過 diff 算法對比出前后變化, 最后批量更新. 沒錯, 很好, 這就是大致過程, 但這里存在著一些隱藏的深層問題值得探討 :

  • React 是如何用 virtual DOM 表示了頁面結(jié)構(gòu), 從而使任何頁面變化都能被 diff 出來?
  • React 是如何 diff 出頁面變化最小的部分?

React 如何表示頁面結(jié)構(gòu)

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: nullprops: { // 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: nulltype: "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')都會被存在這里, 如果這個元素有子元素(包括文本內(nèi)容), props就會有children屬性, 存儲子元素; children屬性是遞歸插入、遞歸更新的依據(jù);

也就是說, 如果元素唯一標識符或者類別或者屬性有變化, 那么它們re-render后對應(yīng)的 key、type 和props里面的屬性也會改變, 前后一對比即可找出變化. 綜上來看, React 這么表示頁面結(jié)構(gòu)確實能夠反映前后所有變化.

那么 React 是如何 diff 的?

React diff 每次只對同一層級的節(jié)點進行比對 :

上圖的數(shù)字表示遍歷更新的次序.

從父節(jié)點開始, 每一層 diff 包括兩個地方

  • element diff—— 前后 render 出來的 element 的對比, 這個對比是為了找出前后節(jié)點是不是同一節(jié)點, 會對比前后render出來的元素它們的 key 和 type. element diff 包括兩個地方, 組件頂層DOM元素對比和子元素的對比:

    組件頂層DOM元素對比 :

    // ReactCompositeComponent.js/updateComponent => _updateRenderedComponent _updateRenderedComponent: function(transaction, context) {// re-render 出elementvar 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 函數(shù):

    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;// 如果前后變化都是字符串、數(shù)字類型的則允許更新if (prevType === 'string' || prevType === 'number') {return nextType === 'string' || nextType === 'number';} else {// 否則檢查 type && keyreturn (nextType === 'object' &&prevElement.type === nextElement.type &&prevElement.key === nextElement.key);} }

    element diff 檢測 type && key 都沒變時會進入下一級更新, 如果變化則直接移除重造新元素, 然后遍歷同級的下一個.

  • subtree diff ——組件頂層DOM元素包裹的所有子元素(也就是props.children里的元素)與之前版本的對比, 這個對比是為了找出同級所有子節(jié)點的變化, 包括移除、新建、同級范圍的移動;

    // 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) {// 發(fā)生了移動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 記錄了所有的移除節(jié)點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 等, 統(tǒng)一放到 updates 數(shù)組中然后批量處理.

And that‘s it !

React 是一個激動人心的庫, 它給我們帶來了前所未有的開發(fā)體驗, 但當(dāng)我們沉浸在使用 React 快速實現(xiàn)需求的喜悅中時, 有必要去探究兩個問題 : Why and How?

為什么 React 會如此流行, 原因是什么? 組件化、快速、足夠簡單、all in js、容易擴展、生態(tài)豐富、社區(qū)強大...

React 反映了哪些思想/理念/思路 ? 狀態(tài)機、webComponents、virtual DOM、virtual stack、異步渲染、多端渲染、單向數(shù)據(jù)流、反應(yīng)式更新、函數(shù)式編程...

React 這些理念/思路受什么啟發(fā) ? 怎么想到的 ? 又怎么實現(xiàn)的? ...

總結(jié)

以上是生活随笔為你收集整理的从源码全面剖析 React 组件更新机制的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網(wǎng)站內(nèi)容還不錯,歡迎將生活随笔推薦給好友。