這篇文章帶給大家的內容是關於虛擬DOM怎麼實現? (程式碼範例),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
本文透過對virtual-dom的源碼進行閱讀和分析,針對Virtual DOM的結構和相關的Diff演算法進行講解,讓讀者能夠對整個資料結構以及相關的Diff演算法有一定的了解。
Virtual DOM中Diff演算法所得到的結果如何映射到真實DOM中,我們將在下一篇部落格揭曉。
本文的主要內容為:
Virtual DOM的結構Virtual DOM的Diff演算法
註:這個Virtual DOM的實作並不是React Virtual DOM的源碼,而是基於virtual-dom)這個函式庫。兩者在原理上類似,這個函式庫更簡單容易理解。相較於這個函式庫,React對Virtual DOM做了進一步的優化和調整,我會在後續的部落格中進行分析。
Virtual DOM的結構
VirtualNode
作為Virtual DOM的元資料結構,VirtualNode位於vnode/vnode.js檔案中。我們截取一部分宣告程式碼來看下內部結構:
function VirtualNode(tagName, properties, children, key, namespace) { this.tagName = tagName this.properties = properties || noProperties //props对象,Object类型 this.children = children || noChildren //子节点,Array类型 this.key = key != null ? String(key) : undefined this.namespace = (typeof namespace === "string") ? namespace : null ... this.count = count + descendants this.hasWidgets = hasWidgets this.hasThunks = hasThunks this.hooks = hooks this.descendantHooks = descendantHooks } VirtualNode.prototype.version = version //VirtualNode版本号,isVnode()检测标志 VirtualNode.prototype.type = "VirtualNode" // VirtualNode类型,isVnode()检测标志
上面就是一個VirtualNode的完整結構,包含了特定的標籤名稱、屬性、子節點等。
VText
VText是一個純文字的節點,對應的是HTML中的純文字。因此,這個屬性也只有text這一個欄位。
function VirtualText(text) { this.text = String(text) } VirtualText.prototype.version = version VirtualText.prototype.type = "VirtualText"
VPatch
VPatch是表示需要對Virtual DOM執行的操作記錄的資料結構。它位於vnode/vpatch.js檔案中。我們來看裡面的具體程式碼:
// 定义了操作的常量,如Props变化,增加节点等 VirtualPatch.NONE = 0 VirtualPatch.VTEXT = 1 VirtualPatch.VNODE = 2 VirtualPatch.WIDGET = 3 VirtualPatch.PROPS = 4 VirtualPatch.ORDER = 5 VirtualPatch.INSERT = 6 VirtualPatch.REMOVE = 7 VirtualPatch.THUNK = 8 module.exports = VirtualPatch function VirtualPatch(type, vNode, patch) { this.type = Number(type) //操作类型 this.vNode = vNode //需要操作的节点 this.patch = patch //需要操作的内容 } VirtualPatch.prototype.version = version VirtualPatch.prototype.type = "VirtualPatch"
其中常數定義了對VNode節點的操作。例如:VTEXT就是增加一個VText節點,PROPS就是目前節點有Props屬性改變。
Virtual DOM的Diff演算法
了解了虛擬DOM中的三個結構,那我們下面來看下Virtual DOM的Diff演算法。
這個Diff演算法是Virtual DOM中最核心的一個演算法。透過輸入初始狀態A(VNode)和最終狀態B(VNode),這個演算法可以得到從A到B的變化步驟(VPatch),根據得到的這一連串步驟,我們可以知道哪些節點需要新增,哪些節點需要刪除,哪些節點的屬性有了變化。在這個Diff演算法中,又分成了三個部分:
VNode的Diff演算法Props的Diff演算法Vnode children的Diff演算法
下面,我們就來一個一個介紹這些Diff演算法。
VNode的Diff演算法
此演算法是針對於單一VNode的比較演算法。它是用於兩個樹中單一節點比較的場景。具體演算法如下,如果不想直接閱讀原始碼的同學也可以翻到下面,會有相關程式碼流程說明供大家參考:
function walk(a, b, patch, index) { if (a === b) { return } var apply = patch[index] var applyClear = false if (isThunk(a) || isThunk(b)) { thunks(a, b, patch, index) } else if (b == null) { // If a is a widget we will add a remove patch for it // Otherwise any child widgets/hooks must be destroyed. // This prevents adding two remove patches for a widget. if (!isWidget(a)) { clearState(a, patch, index) apply = patch[index] } apply = appendPatch(apply, new VPatch(VPatch.REMOVE, a, b)) } else if (isVNode(b)) { if (isVNode(a)) { if (a.tagName === b.tagName && a.namespace === b.namespace && a.key === b.key) { var propsPatch = diffProps(a.properties, b.properties) if (propsPatch) { apply = appendPatch(apply, new VPatch(VPatch.PROPS, a, propsPatch)) } apply = diffChildren(a, b, patch, apply, index) } else { apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) applyClear = true } } else { apply = appendPatch(apply, new VPatch(VPatch.VNODE, a, b)) applyClear = true } } else if (isVText(b)) { if (!isVText(a)) { apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) applyClear = true } else if (a.text !== b.text) { apply = appendPatch(apply, new VPatch(VPatch.VTEXT, a, b)) } } else if (isWidget(b)) { if (!isWidget(a)) { applyClear = true } apply = appendPatch(apply, new VPatch(VPatch.WIDGET, a, b)) } if (apply) { patch[index] = apply } if (applyClear) { clearState(a, patch, index) } }
程式碼具體邏輯如下:
如果a和b這兩個VNode全等,則認為沒有修改,直接回傳。
如果其中有一個是thunk,則使用thunk的比較方法thunks。
如果a是widget且b為空,那麼透過遞歸將a和它的子節點的remove操作加入到patch中。
如果b是VNode的話,
如果a也是VNode,那麼比較tagName、namespace、key,如果相同則比較兩個VNode的Props(用下面提到的diffProps演算法),同時比較兩個VNode的children(用下面提到的diffChildren演算法);如果不同則直接將b節點的insert操作加入到patch中,同時將標記位置為true。
如果a不是VNode,那麼直接將b節點的insert操作加入patch中,同時將標記位置為true。
如果b是VText的話,看a的類型是否為VText,如果不是,則將VText操作新增至patch中,並且將標誌位元設為true;如果是且文字內容不同,則將VText操作加入到patch中。
如果b是Widget的話,看a的型別是否為widget,如果是,將標誌位元設為true。不論a類型為什麼,都將Widget操作加入patch。
檢查標誌位,如果標識為為true,那麼透過遞歸將a和它的子節點的remove操作加入到patch中。
這就是單一VNode節點的diff演算法全過程。這個演算法是整個diff演算法的入口,兩棵樹的比較就是從這個演算法開始的。
Prpps的Diff演算法
看完了單一VNode節點的diff演算法,我們來看下上面提到的diffProps
演算法。
此演算法是針對於兩個比較的VNode節點的Props比較演算法。它是用於兩個場景中key值和標籤名都相同的情況。具體演算法如下,如果不想直接閱讀原始碼的同學也可以翻到下面,會有相關程式碼流程說明供大家參考:
function diffProps(a, b) { var diff for (var aKey in a) { if (!(aKey in b)) { diff = diff || {} diff[aKey] = undefined } var aValue = a[aKey] var bValue = b[aKey] if (aValue === bValue) { continue } else if (isObject(aValue) && isObject(bValue)) { if (getPrototype(bValue) !== getPrototype(aValue)) { diff = diff || {} diff[aKey] = bValue } else if (isHook(bValue)) { diff = diff || {} diff[aKey] = bValue } else { var objectDiff = diffProps(aValue, bValue) if (objectDiff) { diff = diff || {} diff[aKey] = objectDiff } } } else { diff = diff || {} diff[aKey] = bValue } } for (var bKey in b) { if (!(bKey in a)) { diff = diff || {} diff[bKey] = b[bKey] } } return diff }
程式碼具體邏輯如下:
-
遍歷
a
物件。- 当key值不存在于
b
,则将此值存储下来,value赋值为undefined
。 - 当此key对应的两个属性都相同时,继续终止此次循环,进行下次循环。
- 当key值对应的value不同且key值对应的两个value都是对象时,判断Prototype值,如果不同则记录key对应的
b
对象的值;如果b
对应的value是hook
的话,记录b的值。 - 上面条件判断都不同且都是对象时,则继续比较key值对应的两个对象(递归)。
- 当有一个不是对象时,直接将
b
对应的value进行记录。
- 当key值不存在于
- 遍历
b
对象,将所有a
对象中不存在的key值对应的对象都记录下来。
整个算法的大致流程如下,因为比较简单,就不画相关流程图了。如果逻辑有些绕的话,可以配合代码食用,效果更佳。
Vnode children的Diff算法
下面让我们来看下最后一个算法,就是关于两个VNode节点的children属性的diffChildren
算法。这个个diff算法分为两个部分,第一部分是将变化后的结果b
的children进行顺序调整的算法,保证能够快速的和a
的children进行比较;第二部分就是将a
的children与重新排序调整后的b
的children进行比较,得到相关的patch。下面,让我们一个一个算法来看。
reorder算法
该算法的作用是将b
节点的children数组进行调整重新排序,让a
和b
两个children之间的diff算法更加节约时间。具体代码如下:
function reorder(aChildren, bChildren) { // O(M) time, O(M) memory var bChildIndex = keyIndex(bChildren) var bKeys = bChildIndex.keys // have "key" prop,object var bFree = bChildIndex.free //don't have "key" prop,array // all children of b don't have "key" if (bFree.length === bChildren.length) { return { children: bChildren, moves: null } } // O(N) time, O(N) memory var aChildIndex = keyIndex(aChildren) var aKeys = aChildIndex.keys var aFree = aChildIndex.free // all children of a don't have "key" if (aFree.length === aChildren.length) { return { children: bChildren, moves: null } } // O(MAX(N, M)) memory var newChildren = [] var freeIndex = 0 var freeCount = bFree.length var deletedItems = 0 // Iterate through a and match a node in b // O(N) time, for (var i = 0 ; i = bFree.length ? bChildren.length : bFree[freeIndex] // Iterate through b and append any new keys // O(M) time for (var j = 0; j = lastFreeIndex) { // Add any leftover non-keyed items newChildren.push(newItem) } } var simulate = newChildren.slice() var simulateIndex = 0 var removes = [] var inserts = [] var simulateItem for (var k = 0; k <p>下面,我们来简单介绍下这个排序算法:</p><ol> <li>检查<code>a</code>和<code>b</code>中的children是否拥有key字段,如果没有,直接返回<code>b</code>的children数组。</li> <li> <p>如果存在,初始化一个数组newChildren,遍历<code>a</code>的children元素。</p> <ol> <li>如果aChildren存在key值,则去bChildren中找对应key值,如果bChildren存在则放入新数组中,不存在则放入一个null值。</li> <li>如果aChildren不存在key值,则从bChildren中不存在key值的第一个元素开始取,放入新数组中。</li> </ol> </li> <li>遍历bChildren,将所有achildren中没有的key值对应的value或者没有key,并且没有放入新数组的子节点放入新数组中。</li> <li>将bChildren和新数组逐个比较,得到从新数组转换到bChildren数组的<code>move</code>操作patch(即<code>remove</code>+<code>insert</code>)。</li> <li>返回新数组和<code>move</code>操作列表。</li> </ol><p>通过上面这个排序算法,我们可以得到一个新的<code>b</code>的children数组。在使用这个数组来进行比较厚,我们可以将两个children数组之间比较的时间复杂度从o(n^2)转换成o(n)。具体的方法和效果我们可以看下面的DiffChildren算法。</p><h3 id="DiffChildren算法">DiffChildren算法</h3><pre class="brush:php;toolbar:false">function diffChildren(a, b, patch, apply, index) { var aChildren = a.children var orderedSet = reorder(aChildren, b.children) var bChildren = orderedSet.children var aLen = aChildren.length var bLen = bChildren.length var len = aLen > bLen ? aLen : bLen for (var i = 0; i <p>通过上面的重新排序算法整理了以后,两个children比较就只需在相同下标的情况下比较了,即aChildren的第N个元素和bChildren的第N个元素进行比较。然后较长的那个元素做<code>insert</code>操作(bChildren)或者<code>remove</code>操作(aChildren)即可。最后,我们将move操作再增加到patch中,就能够抵消我们在reorder时对整个数组的操作。这样只需要一次便利就得到了最终的patch值。</p><p style="white-space: normal;">总结</p><p>整个Virtual DOM的diff算法设计的非常精巧,通过三个不同的分部算法来实现了VNode、Props和Children的diff算法,将整个Virtual DOM的的diff操作分成了三类。同时三个算法又互相递归调用,对两个Virtual DOM数做了一次(伪)深度优先的递归比较。</p><p> </p>
以上是虛擬DOM怎麼實現? (程式碼範例)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

從C/C 轉向JavaScript需要適應動態類型、垃圾回收和異步編程等特點。 1)C/C 是靜態類型語言,需手動管理內存,而JavaScript是動態類型,垃圾回收自動處理。 2)C/C 需編譯成機器碼,JavaScript則為解釋型語言。 3)JavaScript引入閉包、原型鍊和Promise等概念,增強了靈活性和異步編程能力。

不同JavaScript引擎在解析和執行JavaScript代碼時,效果會有所不同,因為每個引擎的實現原理和優化策略各有差異。 1.詞法分析:將源碼轉換為詞法單元。 2.語法分析:生成抽象語法樹。 3.優化和編譯:通過JIT編譯器生成機器碼。 4.執行:運行機器碼。 V8引擎通過即時編譯和隱藏類優化,SpiderMonkey使用類型推斷系統,導致在相同代碼上的性能表現不同。

JavaScript在現實世界中的應用包括服務器端編程、移動應用開發和物聯網控制:1.通過Node.js實現服務器端編程,適用於高並發請求處理。 2.通過ReactNative進行移動應用開發,支持跨平台部署。 3.通過Johnny-Five庫用於物聯網設備控制,適用於硬件交互。

我使用您的日常技術工具構建了功能性的多租戶SaaS應用程序(一個Edtech應用程序),您可以做同樣的事情。 首先,什麼是多租戶SaaS應用程序? 多租戶SaaS應用程序可讓您從唱歌中為多個客戶提供服務

本文展示了與許可證確保的後端的前端集成,並使用Next.js構建功能性Edtech SaaS應用程序。 前端獲取用戶權限以控制UI的可見性並確保API要求遵守角色庫

JavaScript是現代Web開發的核心語言,因其多樣性和靈活性而廣泛應用。 1)前端開發:通過DOM操作和現代框架(如React、Vue.js、Angular)構建動態網頁和單頁面應用。 2)服務器端開發:Node.js利用非阻塞I/O模型處理高並發和實時應用。 3)移動和桌面應用開發:通過ReactNative和Electron實現跨平台開發,提高開發效率。

JavaScript的最新趨勢包括TypeScript的崛起、現代框架和庫的流行以及WebAssembly的應用。未來前景涵蓋更強大的類型系統、服務器端JavaScript的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。

JavaScript是現代Web開發的基石,它的主要功能包括事件驅動編程、動態內容生成和異步編程。 1)事件驅動編程允許網頁根據用戶操作動態變化。 2)動態內容生成使得頁面內容可以根據條件調整。 3)異步編程確保用戶界面不被阻塞。 JavaScript廣泛應用於網頁交互、單頁面應用和服務器端開發,極大地提升了用戶體驗和跨平台開發的靈活性。


熱AI工具

Undresser.AI Undress
人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover
用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

VSCode Windows 64位元 下載
微軟推出的免費、功能強大的一款IDE編輯器

Dreamweaver CS6
視覺化網頁開發工具

WebStorm Mac版
好用的JavaScript開發工具

Safe Exam Browser
Safe Exam Browser是一個安全的瀏覽器環境,安全地進行線上考試。該軟體將任何電腦變成一個安全的工作站。它控制對任何實用工具的訪問,並防止學生使用未經授權的資源。

禪工作室 13.0.1
強大的PHP整合開發環境