摘要:最終的更新策略都在中。按以上兩個狀態(tài)的,那么遍歷完就會記錄下需要做兩步變更新增一個節(jié)點插入到第二個位置,刪除原來第二個位置上的。例遍歷結(jié)果第二個節(jié)點新增一個,第四個節(jié)點新增一個,刪除第二個。基本上之后到最終的變化的過程就是這么結(jié)束了。
說在前面,著重梳理實際更新組件和 dom 部分的代碼,但是關(guān)于異步,transaction,批量合并新狀態(tài)等新細(xì)節(jié)只描述步驟。一來因為這些細(xì)節(jié)在讀源碼的時候只讀了部分,二來如果要把這些都寫出來要寫老長老長。
真實的 setState 的過程:
setState( partialState ) { // 1. 通過組件對象獲取到渲染對象 var internalInstance = ReactInstanceMap.get(publicInstance); // 2. 把新的狀態(tài)放在渲染對象的 _pendingStateQueue 里面 internalInstance._pendingStateQueue.push( partialState ) // 3. 查看下是否正在批量更新 // 3.1. 如果正在批量更新,則把當(dāng)前這個組件認(rèn)為是臟組件,把其渲染對象保存到 dirtyComponents 數(shù)組中 // 3.2. 如果可以批量更新,則調(diào)用 ReactDefaultBatchingStrategyTransaction 開啟更新事務(wù),進行真正的 vdom diff。 // | // v // internalInstance.updateComponent( partialState ) }
updateComponent 方法的說明:
updateComponent( partialState ) { // 源碼中 partialState 是從 this._pendingStateQueue 中獲取的,這里簡化了狀態(tài)隊列的東西,假設(shè)直接從外部傳入 var inst = this._instance; var nextState = Object.assign( {}, inst.state, partialState ); // 獲得組件對象,準(zhǔn)備更新,先調(diào)用生命周期函數(shù) // 調(diào)用 shouldComponentUpdate 看看是否需要更新組件(這里先忽略 props 和 context的更新) if ( inst.shouldComponentUpdate(inst.props, nextState, nextContext) ) { // 更新前調(diào)用 componentWillUpdate isnt.componentWillUpdate( inst.props, nextState, nextContext ); inst.state = nextState; // 生成新的 vdom var nextRenderedElement = inst.render(); // 通過上一次的渲染對象獲取上一次生成的 vdom var prevComponentInstance = this._renderedComponent; // render 中的根節(jié)點的渲染對象 var prevRenderedElement = prevComponentInstance._currentElement; // 上一次的根節(jié)點的 vdom // 通過比較新舊 vdom node 來決定是更新 dom node 還是根據(jù)最新的 vdom node 生成一份真實 dom node 替換掉原來的 if ( shouldUpdateReactComponent(prevRenderedElement, nextRenderedElement) ) { // 更新 dom node prevComponentInstance.receiveComponent( nextRenderedElement ) } else { // 生成新的 dom node 替換原來的(以下是簡化版,只為了說明流程) var oldHostNode = ReactReconciler.getHostNode( prevComponentInstance ); // 根據(jù)新的 vdom 生成新的渲染對象 var child = instantiateReactComponent( nextRenderedElement ); this._renderedComponent = child; // 生成新的 dom node var nextMarkup = child.mountComponent(); // 替換原來的 dom node oldHostNode.empty(); oldHostNode.appendChild( nextMarkup ) } } }
接下來看下 shouldUpdateReactComponent 方法:
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 { return ( nextType === "object" && prevElement.type === nextElement.type && prevElement.key === nextElement.key ); } }
基本的思路就是比較當(dāng)前 vdom 節(jié)點的類型,如果一致則更新,如果不一致則重新生成一份新的節(jié)點替換掉原來的。好了回到剛剛跟新 dom node這條路 prevComponentInstance.receiveComponent( nextRenderedElement ),即 render 里面根元素的渲染對象的 receiveComponent 方法做了最后的更新 dom 的工作。如果根節(jié)點的渲染對象是組件即 ReactCompositeComponent.receiveComponent,如果根節(jié)點是內(nèi)置對象(html 元素)節(jié)點即 ReactDOMComponent.receiveComponent。ReactCompositeComponent.receiveComponent 最終還是調(diào)用的上面提到的 updateComponent 循環(huán)去生成 render 中的 vdom,這里就先不深究了。最終 html dom node 的更新策略都在 ReactDOMComponent.receiveComponent 中。
class ReactDOMComponent { // @param {nextRenderedElement} 新的 vdom node receiveComponent( nextRenderedElement ) { var prevElement = this._currentElement; this._currentElement = nextRenderedElement; var lastProps = prevElement.props; var nextProps = this._currentElement.props; var lastChildren = lastProps.children; var nextChildren = nextProps.children; /* 更新 props _updateDOMProperties 方法做了下面兩步 1. 記錄下 lastProps 中有的,nextProps 沒有的,刪除 2. 記錄下 nextProps 中有的,且與 lastProps中不同的屬性,setAttribute 之 */ this._updateDOMProperties(lastProps, nextProps, transaction); /* 迭代更新子節(jié)點,源代碼中是 this._updateDOMChildren(lastProps, nextProps, transaction, context); 以下把 _updateDOMChildren 方法展開,對于子節(jié)點類型的判斷源碼比較復(fù)雜,這里只針對string|number和非string|number做一個簡單的流程示例 */ // 1. 如果子節(jié)點從有到無,則刪除子節(jié)點 if ( lastChildren != null && nextChildren == null ) { if ( typeof lastChildren === "string" | "number" /* 偽代碼 */ ) { this.updateTextContent(""); } else { this.updateChildren( null, transaction, context ); } } // 2. 如果新的子節(jié)點相對于老的是有變化的 if ( nextChildren != null ) { if ( typeof lastChildren === "string" | "number" && lastChildren !== nextChildren /* 偽代碼 */ ) { this.updateTextContent("" + nextChildren); } else if ( lastChildren !== nextChildren ) { this.updateChildren( nextChildren, transaction, context ); } } } }
this.updateChildren( nextChildren, transaction, context ) 中是真正的 diff 算法,就不以代碼來說了(因為光靠代碼很難說明清楚)
先來看最簡單的情況:
例A:
按節(jié)點順序開始遍歷 nextChildren(遍歷的過程中記錄下需要對節(jié)點做哪些變更,等遍歷完統(tǒng)一執(zhí)行最終的 dom 操作),相同位置如果碰到和 prevChildren 中 tag 一樣的元素認(rèn)為不需要對節(jié)點進行刪除,只需要更新節(jié)點的 attr,如果碰到 tag 不一樣,則按照新的 vdom 中的節(jié)點重新生成一個節(jié)點,并把 prevChildren 中相同位置老節(jié)點刪除。按以上兩個狀態(tài)的 vdom tree,那么遍歷完就會記錄下需要做兩步 dom 變更:新增一個 span 節(jié)點插入到第二個位置,刪除原來第二個位置上的 div。
再來看兩個例子:
例B:
遍歷結(jié)果:第二個節(jié)點新增一個span,刪除第二個div和第四個div。
例C:
遍歷結(jié)果:第二個節(jié)點新增一個span,第四個節(jié)點新增一個div,刪除第二個div。
我們看到對于例C來說其實最便利的方法就是把 span 插入到第二的位置上,然后其他div只要做 attr 的更新而不需要再進行位置的增刪,如果 attr 都沒有變化,那么后兩個 div 根本不需要變化。但是按例A里面的算法,我們需要進行好幾步的 dom 操作。這是為算法減少時間復(fù)雜度,做了妥協(xié)。但是 react 對節(jié)點引入了 key 這個關(guān)鍵屬性幫助優(yōu)化這種情況。假設(shè)我們給所有節(jié)點都添加了唯一的 key 屬性,如下面例D:
例D:
我們在遍歷過程中對所要記錄的東西進行優(yōu)化,在某個位置碰到有 key 的節(jié)點我們?nèi)?prevChildren 中找有沒有對應(yīng)的節(jié)點,如果有,則我們會比較當(dāng)前節(jié)點在前后兩個 tree 中相對位置。如果相對位置沒有變化,則不需要做dom的增刪移,而只需要更新。如果位置不一樣則需要記錄把這個節(jié)點從老的位置移動到新的位置(具體算法需要借助前一次dom變化的記錄這里不詳述)。這樣從例C到例D的優(yōu)化減少了 dom 節(jié)點的增刪。
但是 react 的這種算法的優(yōu)化也帶來了一種極端的情況:
例E:
遍歷結(jié)果:3次節(jié)點位置移動:2到1,1到2,0到3。
但是其實這里只需要更新每個節(jié)點的 attr,他們的位置根本不需要做變化。所以如果要給元素指定 key 最好避免元素的位置有太多太大的躍遷變化。
基本上 setState 之后到最終的 dom 變化的過程就是這么結(jié)束了。
后記:
梳理的比較簡單,很多細(xì)節(jié)我沒有精力作一一的總結(jié),因為我自己看源碼看了好久,代碼中涉及到很多異步,事務(wù)等等干擾項,然后我自己又不想過多的借助現(xiàn)有的資料-_-。當(dāng)我快要把最后一點寫完的時候發(fā)現(xiàn) pure render 專欄的作者陳屹出了一本《深入React技術(shù)棧》里面有相當(dāng)詳細(xì)的源碼分析,所以我感覺我這篇“白寫”了,貼出這本書就可以了,不過陳屹的這本書是良心之作,必須安利下。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/83011.html
摘要:另外本文中會介紹一個通過類繼承方式定義的組件的生命周期,以及在各個生命周期函數(shù)中能做什么,不能或盡量不要做什么。各個生命周期函數(shù)介紹及使用經(jīng)驗。獲取組件的初始內(nèi)部狀態(tài)在中。該聲明周期函數(shù)可能在兩種情況下被調(diào)用組件接收到了新的屬性。 文章標(biāo)題總算是可以正常一點了…… 通過之前的文章我們已經(jīng)知道:在 React 體系中所謂的 在 JavaScript 中編寫 HTML 代碼 指的是 Rea...
摘要:另外本文中會介紹一個通過類繼承方式定義的組件的生命周期,以及在各個生命周期函數(shù)中能做什么,不能或盡量不要做什么。各個生命周期函數(shù)介紹及使用經(jīng)驗。獲取組件的初始內(nèi)部狀態(tài)在中。該聲明周期函數(shù)可能在兩種情況下被調(diào)用組件接收到了新的屬性。 文章標(biāo)題總算是可以正常一點了…… 通過之前的文章我們已經(jīng)知道:在 React 體系中所謂的 在 JavaScript 中編寫 HTML 代碼 指的是 Rea...
摘要:將注意力集中保持在核心庫,而將其他功能如路由和全局狀態(tài)管理交給相關(guān)的庫。此示例使用類似的語法,稱為。執(zhí)行更快,因為它在編譯為代碼后進行了優(yōu)化。基于的模板使得將已有的應(yīng)用逐步遷移到更為容易。 前言 因為沒有明確的界定,這里不討論正確與否,只表達(dá)個人對前端MV*架構(gòu)模式理解看法,再比較React和Vue兩種框架不同.寫完之后我知道這文章好水,特別是框架對比部分都是別人說爛的,而我也是打算把...
摘要:是用戶建立客戶端應(yīng)用的前端架構(gòu),它通過利用一個單向的數(shù)據(jù)流補充了的組合視圖組件,這更是一種模式而非正式框架,你能夠無需許多新代碼情況下立即開始使用。結(jié)構(gòu)和數(shù)據(jù)流一個單向數(shù)據(jù)流是模式的核心。 Flux是Facebook用戶建立客戶端Web應(yīng)用的前端架構(gòu),它通過利用一個單向的數(shù)據(jù)流補充了React的組合視圖組件,這更是一種模式而非正式框架,你能夠無需許多新代碼情況下立即開始使用Flux。 ...
摘要:什么是虛擬在中,執(zhí)行的結(jié)果得到的并不是真正的節(jié)點,結(jié)果僅僅是輕量級的對象,我們稱之為。后來產(chǎn)出的架構(gòu)模式,期望從代碼組織方式來降低維護難度。 1、什么是虛擬DOM 在React中,render執(zhí)行的結(jié)果得到的并不是真正的DOM節(jié)點,結(jié)果僅僅是輕量級的JavaScript對象,我們稱之為virtual DOM。 簡單的說,其實所謂的virtual DOM就是JavaScript對象到H...
摘要:什么是虛擬在中,執(zhí)行的結(jié)果得到的并不是真正的節(jié)點,結(jié)果僅僅是輕量級的對象,我們稱之為。后來產(chǎn)出的架構(gòu)模式,期望從代碼組織方式來降低維護難度。 1、什么是虛擬DOM 在React中,render執(zhí)行的結(jié)果得到的并不是真正的DOM節(jié)點,結(jié)果僅僅是輕量級的JavaScript對象,我們稱之為virtual DOM。 簡單的說,其實所謂的virtual DOM就是JavaScript對象到H...
閱讀 2212·2021-09-30 09:47
閱讀 960·2021-08-27 13:01
閱讀 2959·2019-08-30 15:54
閱讀 3685·2019-08-30 15:53
閱讀 825·2019-08-29 14:07
閱讀 711·2019-08-28 18:16
閱讀 795·2019-08-26 18:37
閱讀 1406·2019-08-26 13:27