這篇文章帶給大家的內容是關於Vue中虛擬dom比較原理的介紹(範例講解),有一定的參考價值,有需要的朋友可以參考一下,希望對你有幫助。
先說為什麼會有虛擬dom比較這個階段,我們知道了Vue是資料驅動視圖(資料的變化會造成視圖的變化),但你發現某個資料改變時,視圖是局部刷新而不是整個重新渲染,如何精準的找到資料對應的視圖並進行更新呢?那就需要拿到資料改變前後的dom結構,找到差異點並進行更新!
虛擬dom實質上是針對真實dom提煉出的簡單物件。就像一個簡單的p包含200多個屬性,但真正需要的可能只有tagName
,所以對真實dom直接操作將大大影響效能!
簡化後的虛擬節點(vnode)大致包含以下屬性:
{ tag: 'p', // 标签名 data: {}, // 属性数据,包括class、style、event、props、attrs等 children: [], // 子节点数组,也是vnode结构 text: undefined, // 文本 elm: undefined, // 真实dom key: undefined // 节点标识 }
虛擬dom的比較,就是找出新節點(vnode )和舊節點(oldVnode)之間的差異,然後對差異進行打補丁(patch)。大致流程如下
整個過程還是比較簡單的,新舊節點如果不相似,直接根據新節點建立dom;如果相似,先是對data比較,包括class、style 、event、props、attrs等,有不同就呼叫對應的update函數,然後是子節點的比較,子節點的比較用到了diff演算法,這應該是這篇文章的重點和困難吧。
值得注意的是,在Children Compare
過程中,如果找到了相似的childVnode
,那麼它們將遞歸進入新的打補丁過程。
原始碼解析
這次的原始碼解析寫簡潔一點,寫太多發現自己都不願意看(┬_┬)
開始
#先來看patch()
函數:
function patch (oldVnode, vnode) { var elm, parent; if (sameVnode(oldVnode, vnode)) { // 相似就去打补丁(增删改) patchVnode(oldVnode, vnode); } else { // 不相似就整个覆盖 elm = oldVnode.elm; parent = api.parentNode(elm); createElm(vnode); if (parent !== null) { api.insertBefore(parent, vnode.elm, api.nextSibling(elm)); removeVnodes(parent, [oldVnode], 0, 0); } } return vnode.elm; }
patch()
函數接收新舊vnode兩個參數,傳入的兩個參數有很大的差異:oldVnode的elm
指向真實dom,而vnode的elm
為undefined...但經過patch()
方法後,vnode的elm
也會指向這個(更新過的)真實dom。
判斷新舊vnode是否相似的sameVnode()
方法很簡單,就是比較tag和key#是否一致。
function sameVnode (a, b) { return a.key === b.key && a.tag === b.tag; }
打補丁
對於新舊vnode不一致的處理方法很簡單,就是根據vnode建立真實dom,取代oldVnode中的elm
插入DOM文檔。
對於新舊vnode一致的處理,就是我們前面常說到的打補丁了。具體什麼是打補丁?看看patchVnode()
方法就知道了:
function patchVnode (oldVnode, vnode) { // 新节点引用旧节点的dom let elm = vnode.elm = oldVnode.elm; const oldCh = oldVnode.children; const ch = vnode.children; // 调用update钩子 if (vnode.data) { updateAttrs(oldVnode, vnode); updateClass(oldVnode, vnode); updateEventListeners(oldVnode, vnode); updateProps(oldVnode, vnode); updateStyle(oldVnode, vnode); } // 判断是否为文本节点 if (vnode.text == undefined) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue) } else if (isDef(ch)) { if (isDef(oldVnode.text)) api.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) } else if (isDef(oldCh)) { removeVnodes(elm, oldCh, 0, oldCh.length - 1) } else if (isDef(oldVnode.text)) { api.setTextContent(elm, '') } } else if (oldVnode.text !== vnode.text) { api.setTextContent(elm, vnode.text) } }
打補丁其實就是呼叫各種updateXXX()
函數,更新真實dom的各個屬性。每個的update函數都類似,就拿updateAttrs()
舉例看看:
function updateAttrs (oldVnode, vnode) { let key, cur, old const elm = vnode.elm const oldAttrs = oldVnode.data.attrs || {} const attrs = vnode.data.attrs || {} // 更新/添加属性 for (key in attrs) { cur = attrs[key] old = oldAttrs[key] if (old !== cur) { if (booleanAttrsDict[key] && cur == null) { elm.removeAttribute(key) } else { elm.setAttribute(key, cur) } } } // 删除新节点不存在的属性 for (key in oldAttrs) { if (!(key in attrs)) { elm.removeAttribute(key) } } }
屬性(Attribute
)的更新函數的大致思路就是:
遍歷vnode屬性,如果和oldVnode不一樣就呼叫
setAttribute()
修改;遍歷oldVnode屬性,如果不在vnode屬性中就呼叫
removeAttribute()
刪除。
你會發現裡面有個booleanAttrsDict[key]
的判斷,是用來判斷在不在布林類型屬性字典中。
['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ......]eg :
<video autoplay></video>
,想關閉自動播放,需要移除該屬性。
所有資料比較完後,就到子節點的比較了。先判斷目前vnode是否為文字節點,如果是文字節點就不用考慮子節點的比較;若是元素節點,就需要分三種情況考慮:
新舊節點都有children ,那就進入子節點的比較(diff演算法);
新節點有children,舊節點沒有,那就循環建立dom節點;
#新節點沒有children,舊節點有,那就循環刪除dom節點。
後面兩種情況都比較簡單,我們直接對第一種情況,子節點的比較進行分析。
diff演算法
子節點比較這部分程式碼比較多,先說說原理後面再貼程式碼。先看一張子節點比較的圖:
圖中的oldCh
和newCh
分別表示新舊子節點數組,它們都有自己的頭尾指標oldStartIdx
,oldEndIdx
,newStartIdx
,newEndIdx
,陣列裡面儲存的是vnode,為了容易理解就用a,b,c,d等代替,它們表示不同類型標籤(p,span, p)的vnode物件。
子節點的比較實質上就是迴圈進行頭尾節點比較。循環結束的標誌是:舊子節點陣列或新子節點陣列遍歷完,(即 oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
)。大概看一下循環流程:
#第一步 頭比較。若相似,舊頭新頭指標後移(即
oldStartIdx
&&newStartIdx
),真實dom不變,進入下一次循環;不相似,進入第二步。第二步 尾尾比較。若相似,舊尾新尾指標前移(即
oldEndIdx--
&&newEndIdx--
),真實dom不變,進入下一次迴圈;不相似,進入第三步。第三步 頭尾比較。若相似,舊頭指標後移,新尾指標前移(即
oldStartIdx
&&newEndIdx--
),未確認dom序列中的頭移到尾,進入下一次循環;不相似,進入第四步。第四步 尾頭比較。若相似,舊尾指標前移,新頭指標後移(即
oldEndIdx--
&&newStartIdx
),未確認dom序列中的尾移到頭,進入下一次循環;不相似,進入第五步。第五步若節點有key且在舊子節點數組中找到sameVnode(tag和key都一致),則將其dom移動到當前真實dom序列的頭部,新頭指標後移(即
newStartIdx
);否則,vnode對應的dom(vnode[newStartIdx].elm
)插入目前真實dom序列的頭部,新頭指標後移(即newStartIdx
)。
先看看沒有key的情況,放個動圖看得更清楚些!
相信看完圖片有更好的理解到diff演算法的精髓,整個過程還是比較簡單的。上圖中一共進入了6次循環,涉及了每一種情況,逐一敘述一下:
第一次是頭頭相似(都是
a
), dom不改變,新舊頭指針均後移。a
節點確認後,真實dom序列為:a,b,c,d,e,f
,未確認dom序列為:b,c,d,e,f
;第二次是尾尾相似(都是
f
),dom不改變,新舊尾指標都前移。f
節點確認後,真實dom序列為:a,b,c,d,e,f
,未確認dom序列為:b,c,d,e
;第三次是頭尾相似(都是
b
),目前剩餘真實dom序列中的頭移到尾,舊頭指標後移,新尾指針前移。b
節點確認後,真實dom序列為:a,c,d,e,b,f
,未確認dom序列為:c,d,e
;-
第四次是尾頭相似(都是
e
),當前剩餘真實dom序列中的尾移到頭,舊尾指針前移,新頭指針後移。e
節點確認後,真實dom序列為:a,e,c,d,b,f
,未確認dom序列為:c,d
; 第五次是均不相似,直接插入未確認dom序列頭部。
g
節點插入後,真實dom序列為:a,e,g,c,d,b,f
,未確認dom序列為:c,d
;第六次是均不相似,直接插入未確認dom序列頭部。
h
節點插入後,真實dom序列為:a,e,g,h,c,d,b,f
,未確認dom序列為:c,d
;
但結束循環後,有兩種情況需要考慮:
新的字節點數組(newCh)被遍歷完(
newStartIdx > newEndIdx
)。那就需要把多餘的舊dom(oldStartIdx -> oldEndIdx
)都刪除,上述例子就是c,d
;新的字節點數組(oldCh)遍歷完(
oldStartIdx > oldEndIdx
)。那就需要把多餘的新dom(newStartIdx -> newEndIdx
)都加進來。
上面说了这么多都是没有key的情况,说添加了:key
可以优化v-for
的性能,到底是怎么回事呢?因为v-for
大部分情况下生成的都是相同tag
的标签,如果没有key标识,那么相当于每次头头比较都能成功。你想想如果你往v-for
绑定的数组头部push数据,那么整个dom将全部刷新一遍(如果数组每项内容都不一样),那加了key
会有什么帮助呢?这边引用一张图:
有key
的情况,其实就是多了一步匹配查找的过程。也就是上面循环流程中的第五步,会尝试去旧子节点数组中找到与当前新子节点相似的节点,减少dom的操作!
有兴趣的可以看看代码:
function updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0 let newStartIdx = 0 let oldEndIdx = oldCh.length - 1 let oldStartVnode = oldCh[0] let oldEndVnode = oldCh[oldEndIdx] let newEndIdx = newCh.length - 1 let newStartVnode = newCh[0] let newEndVnode = newCh[newEndIdx] let oldKeyToIdx, idxInOld, elmToMove, before while (oldStartIdx oldEndIdx) { before = isUndef(newCh[newEndIdx+1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) } else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
以上是Vue中虛擬dom比較原理的介紹(範例講解)的詳細內容。更多資訊請關注PHP中文網其他相關文章!

Python和JavaScript在社區、庫和資源方面的對比各有優劣。 1)Python社區友好,適合初學者,但前端開發資源不如JavaScript豐富。 2)Python在數據科學和機器學習庫方面強大,JavaScript則在前端開發庫和框架上更勝一籌。 3)兩者的學習資源都豐富,但Python適合從官方文檔開始,JavaScript則以MDNWebDocs為佳。選擇應基於項目需求和個人興趣。

從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的發展、人工智能和機器學習的擴展以及物聯網和邊緣計算的潛力。


熱AI工具

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

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

Undress AI Tool
免費脫衣圖片

Clothoff.io
AI脫衣器

AI Hentai Generator
免費產生 AI 無盡。

熱門文章

熱工具

SublimeText3漢化版
中文版,非常好用

SAP NetWeaver Server Adapter for Eclipse
將Eclipse與SAP NetWeaver應用伺服器整合。

Dreamweaver Mac版
視覺化網頁開發工具

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

MinGW - Minimalist GNU for Windows
這個專案正在遷移到osdn.net/projects/mingw的過程中,你可以繼續在那裡關注我們。 MinGW:GNU編譯器集合(GCC)的本機Windows移植版本,可自由分發的導入函式庫和用於建置本機Windows應用程式的頭檔;包括對MSVC執行時間的擴展,以支援C99功能。 MinGW的所有軟體都可以在64位元Windows平台上運作。