小編給大家分享一下Vue中虛擬dom比較原理是什么,希望大家閱讀完這篇文章后大所收獲,下面讓我們一起去探討吧!
創(chuàng)新互聯(lián)主營良慶網(wǎng)站建設的網(wǎng)絡公司,主營網(wǎng)站建設方案,app軟件定制開發(fā),良慶h5成都小程序開發(fā)搭建,良慶網(wǎng)站營銷推廣歡迎良慶等地區(qū)企業(yè)咨詢先說一下為什么會有虛擬dom比較這一階段,我們知道了Vue是數(shù)據(jù)驅動視圖(數(shù)據(jù)的變化將引起視圖的變化),但你發(fā)現(xiàn)某個數(shù)據(jù)改變時,視圖是局部刷新而不是整個重新渲染,如何精準的找到數(shù)據(jù)對應的視圖并進行更新呢?那就需要拿到數(shù)據(jù)改變前后的dom結構,找到差異點并進行更新!
虛擬dom實質上是針對真實dom提煉出的簡單對象。就像一個簡單的p包含200多個屬性,但真正需要的可能只有tagName,所以對真實dom直接操作將大大影響性能!

簡化后的虛擬節(jié)點(vnode)大致包含以下屬性:
{
tag: 'p', // 標簽名
data: {}, // 屬性數(shù)據(jù),包括class、style、event、props、attrs等
children: [], // 子節(jié)點數(shù)組,也是vnode結構
text: undefined, // 文本
elm: undefined, // 真實dom
key: undefined // 節(jié)點標識
}虛擬dom的比較,就是找出新節(jié)點(vnode)和舊節(jié)點(oldVnode)之間的差異,然后對差異進行打補丁(patch)。大致流程如下

整個過程還是比較簡單的,新舊節(jié)點如果不相似,直接根據(jù)新節(jié)點創(chuàng)建dom;如果相似,先是對data比較,包括class、style、event、props、attrs等,有不同就調用對應的update函數(shù),然后是對子節(jié)點的比較,子節(jié)點的比較用到了diff算法,這應該是這篇文章的重點和難點吧。
值得注意的是,在Children Compare過程中,如果找到了相似的childVnode,那它們將遞歸進入新的打補丁過程。
這次的源碼解析寫簡潔一點,寫太多發(fā)現(xiàn)自己都不愿意看 (┬_┬)
開始先來看patch()函數(shù):
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()函數(shù)接收新舊vnode兩個參數(shù),傳入的這兩個參數(shù)有個很大的區(qū)別:oldVnode的elm指向真實dom,而vnode的elm為undefined...但經(jīng)過patch()方法后,vnode的elm也將指向這個(更新過的)真實dom。
判斷新舊vnode是否相似的sameVnode()方法很簡單,就是比較tag和key是否一致。
function sameVnode (a, b) {
return a.key === b.key && a.tag === b.tag;
}打補丁對于新舊vnode不一致的處理方法很簡單,就是根據(jù)vnode創(chuàng)建真實dom,代替oldVnode中的elm插入DOM文檔。
對于新舊vnode一致的處理,就是我們前面經(jīng)常說到的打補丁了。具體什么是打補丁?看看patchVnode()方法就知道了:
function patchVnode (oldVnode, vnode) {
// 新節(jié)點引用舊節(jié)點的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);
}
// 判斷是否為文本節(jié)點
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()函數(shù),更新真實dom的各個屬性。每個的update函數(shù)都類似,就拿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)
}
}
}
// 刪除新節(jié)點不存在的屬性
for (key in oldAttrs) {
if (!(key in attrs)) {
elm.removeAttribute(key)
}
}
}屬性(Attribute)的更新函數(shù)的大致思路就是:
遍歷vnode屬性,如果和oldVnode不一樣就調用setAttribute()修改;
遍歷oldVnode屬性,如果不在vnode屬性中就調用removeAttribute()刪除。
你會發(fā)現(xiàn)里面有個booleanAttrsDict[key]的判斷,是用于判斷在不在布爾類型屬性字典中。
['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ......]eg:
<video autoplay></video>,想關閉自動播放,需要移除該屬性。
所有數(shù)據(jù)比較完后,就到子節(jié)點的比較了。先判斷當前vnode是否為文本節(jié)點,如果是文本節(jié)點就不用考慮子節(jié)點的比較;若是元素節(jié)點,就需要分三種情況考慮:
新舊節(jié)點都有children,那就進入子節(jié)點的比較(diff算法);
新節(jié)點有children,舊節(jié)點沒有,那就循環(huán)創(chuàng)建dom節(jié)點;
新節(jié)點沒有children,舊節(jié)點有,那就循環(huán)刪除dom節(jié)點。
后面兩種情況都比較簡單,我們直接對第一種情況,子節(jié)點的比較進行分析。
diff算法子節(jié)點比較這部分代碼比較多,先說說原理后面再貼代碼。先看一張子節(jié)點比較的圖:

圖中的oldCh和newCh分別表示新舊子節(jié)點數(shù)組,它們都有自己的頭尾指針oldStartIdx,oldEndIdx,newStartIdx,newEndIdx,數(shù)組里面存儲的是vnode,為了容易理解就用a,b,c,d等代替,它們表示不同類型標簽(p,span,p)的vnode對象。
子節(jié)點的比較實質上就是循環(huán)進行頭尾節(jié)點比較。循環(huán)結束的標志就是:舊子節(jié)點數(shù)組或新子節(jié)點數(shù)組遍歷完,(即oldStartIdx > oldEndIdx || newStartIdx > newEndIdx)。大概看一下循環(huán)流程:
第一步頭頭比較。若相似,舊頭新頭指針后移(即oldStartIdx++&&newStartIdx++),真實dom不變,進入下一次循環(huán);不相似,進入第二步。
第二步尾尾比較。若相似,舊尾新尾指針前移(即oldEndIdx--&&newEndIdx--),真實dom不變,進入下一次循環(huán);不相似,進入第三步。
第三步頭尾比較。若相似,舊頭指針后移,新尾指針前移(即oldStartIdx++&&newEndIdx--),未確認dom序列中的頭移到尾,進入下一次循環(huán);不相似,進入第四步。
第四步尾頭比較。若相似,舊尾指針前移,新頭指針后移(即oldEndIdx--&&newStartIdx++),未確認dom序列中的尾移到頭,進入下一次循環(huán);不相似,進入第五步。
第五步 若節(jié)點有key且在舊子節(jié)點數(shù)組中找到sameVnode(tag和key都一致),則將其dom移動到當前真實dom序列的頭部,新頭指針后移(即newStartIdx++);否則,vnode對應的dom(vnode[newStartIdx].elm)插入當前真實dom序列的頭部,新頭指針后移(即newStartIdx++)。
先看看沒有key的情況,放個動圖看得更清楚些!

相信看完圖片有更好的理解到diff算法的精髓,整個過程還是比較簡單的。上圖中一共進入了6次循環(huán),涉及了每一種情況,逐個敘述一下:
第一次是頭頭相似(都是a),dom不改變,新舊頭指針均后移。a節(jié)點確認后,真實dom序列為:a,b,c,d,e,f,未確認dom序列為:b,c,d,e,f;
第二次是尾尾相似(都是f),dom不改變,新舊尾指針均前移。f節(jié)點確認后,真實dom序列為:a,b,c,d,e,f,未確認dom序列為:b,c,d,e;
第三次是頭尾相似(都是b),當前剩余真實dom序列中的頭移到尾,舊頭指針后移,新尾指針前移。b節(jié)點確認后,真實dom序列為:a,c,d,e,b,f,未確認dom序列為:c,d,e;
第四次是尾頭相似(都是e),當前剩余真實dom序列中的尾移到頭,舊尾指針前移,新頭指針后移。e節(jié)點確認后,真實dom序列為:a,e,c,d,b,f,未確認dom序列為:c,d;
第五次是均不相似,直接插入到未確認dom序列頭部。g節(jié)點插入后,真實dom序列為:a,e,g,c,d,b,f,未確認dom序列為:c,d;
第六次是均不相似,直接插入到未確認dom序列頭部。h節(jié)點插入后,真實dom序列為:a,e,g,h,c,d,b,f,未確認dom序列為:c,d;
但結束循環(huán)后,有兩種情況需要考慮:
新的字節(jié)點數(shù)組(newCh)被遍歷完(newStartIdx > newEndIdx)。那就需要把多余的舊dom(oldStartIdx -> oldEndIdx)都刪除,上述例子中就是c,d;
新的字節(jié)點數(shù)組(oldCh)被遍歷完(oldStartIdx > oldEndIdx)。那就需要把多余的新dom(newStartIdx -> newEndIdx)都添加。
上面說了這么多都是沒有key的情況,說添加了:key可以優(yōu)化v-for的性能,到底是怎么回事呢?因為v-for大部分情況下生成的都是相同tag的標簽,如果沒有key標識,那么相當于每次頭頭比較都能成功。你想想如果你往v-for綁定的數(shù)組頭部push數(shù)據(jù),那么整個dom將全部刷新一遍(如果數(shù)組每項內(nèi)容都不一樣),那加了key會有什么幫助呢?這邊引用一張圖:

有key的情況,其實就是多了一步匹配查找的過程。也就是上面循環(huán)流程中的第五步,會嘗試去舊子節(jié)點數(shù)組中找到與當前新子節(jié)點相似的節(jié)點,減少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 && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // 未定義表示被移動過
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
} else if (sameVnode(oldStartVnode, newStartVnode)) { // 頭頭相似
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) { // 尾尾相似
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) { // 頭尾相似
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.elm, api.nextSibling(oldEndVnode.elm))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) { // 尾頭相似
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 根據(jù)舊子節(jié)點的key,生成map映射
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 在舊子節(jié)點數(shù)組中,找到和newStartVnode相似節(jié)點的下標
idxInOld = oldKeyToIdx[newStartVnode.key]
if (isUndef(idxInOld)) {
// 沒有key,創(chuàng)建并插入dom
api.insertBefore(parentElm, createElm(newStartVnode), oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
} else {
// 有key,找到對應dom ,移動該dom并在oldCh中置為undefined
elmToMove = oldCh[idxInOld]
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = undefined
api.insertBefore(parentElm, elmToMove.elm, oldStartVnode.elm)
newStartVnode = newCh[++newStartIdx]
}
}
}
// 循環(huán)結束時,刪除/添加多余dom
if (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比較原理是什么有了一定的了解,想了解更多相關知識,歡迎關注創(chuàng)新互聯(lián)網(wǎng)站制作公司行業(yè)資訊頻道,感謝各位的閱讀!
新聞名稱:Vue中虛擬dom比較原理是什么-創(chuàng)新互聯(lián)
文章分享:http://chinadenli.net/article26/cohecg.html
成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供移動網(wǎng)站建設、Google、建站公司、App開發(fā)、自適應網(wǎng)站、外貿(mào)建站
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉載,或轉載時需注明來源: 創(chuàng)新互聯(lián)