이 글의 내용은 Vue의 가상 돔 비교 원리에 대한 소개입니다(예제 설명). 도움이 필요한 친구들이 참고할 수 있기를 바랍니다.
먼저 가상 DOM 비교 단계가 있는 이유에 대해 이야기해 보겠습니다. Vue는 데이터 중심 뷰(데이터 변경으로 인해 뷰 변경이 발생함)라는 것을 알고 있지만 특정 데이터가 변경되면 뷰가 변경된다는 것을 알게 됩니다. 전체 뷰가 아닌 로컬로 새로 고쳐집니다. 데이터에 해당하는 뷰를 정확하게 찾고 다시 렌더링할 때 업데이트하는 방법은 무엇입니까? 그런 다음 데이터 변경 전후의 DOM 구조를 가져와 차이점을 찾아 업데이트해야 합니다!
가상 DOM은 본질적으로 실제 DOM에서 추출된 단순 객체입니다. 단순한 p에 200개 이상의 속성이 포함되어 있지만 실제로 필요한 것은 tagName
뿐이므로 실제 DOM에 대한 직접 작업은 성능에 큰 영향을 미칩니다! 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)에는 대략 다음과 같은 속성이 포함됩니다.
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) } }
을 사용합니다. 이것이 이 글의 초점이자 난이도입니다.
Children Compare
프로세스 중에 유사한 childVnode
가 발견되면 새 패치 프로세스를 시작한다는 점에 주목할 필요가 있습니다.
시작 patch() code>함수:
rrreeepatch()
함수는 이전 vnode와 새 vnode의 두 매개변수를 받습니다. 전달된 두 매개변수 사이에는 큰 차이가 있습니다. oldVnode의 elm은 Real dom을 가리키고 vnode의 elm
은 정의되지 않았습니다... 하지만 patch()
메서드를 전달한 후 vnode의 elm
code>는 또한 이 (과거 업데이트) 실제 DOM을 가리킬 것입니다. 이전 vnode와 새 vnode가 유사한지 확인하는 sameVnode()
메서드는 매우 간단합니다. 즉,
와 key
가 일치하는지 비교하는 것입니다. 🎜rrreee🎜Patch🎜🎜 🎜이전 vnode와 새 vnode 간의 불일치🎜에 대한 해결책은 매우 간단합니다. 즉, vnode를 기반으로 실제 DOM을 만들고elm
대신 DOM 문서를 삽입하는 것입니다. oldVnode. 🎜🎜🎜이전 vnode와 새 vnode를 일관되게🎜 만드는 과정은 앞서 자주 언급했던 패치입니다. 패치란 정확히 무엇입니까? patchVnode()
메서드를 보면 다음과 같은 사실을 알 수 있습니다. 🎜rrreee🎜Patching은 실제로 다양한 updateXXX()
함수를 호출하여 실제 DOM의 다양한 속성을 업데이트합니다. 각 업데이트 함수는 유사합니다. updateAttrs()
를 예로 들면: 🎜rrreee🎜속성(Attribute
)의 업데이트 함수에 대한 일반적인 개념은 다음과 같습니다. 🎜🎜 vnode 속성을 탐색하고, oldVnode와 다른 경우 setAttribute()
를 호출하여 수정합니다. 🎜🎜🎜🎜oldVnode 속성을 탐색하고, vnode 속성에 없으면 다음을 호출합니다. removeAttribute()
를 사용하여 삭제하세요. 🎜🎜🎜🎜내부에는 Boolean 유형 속성 사전에 있는지 판단하는 데 사용되는 booleanAttrsDict[key]
판단이 있음을 알 수 있습니다. 🎜['allowfullscreen', 'async', 'autofocus', 'autoplay', 'checked', 'compact', 'controls', 'declare', ...]🎜eg:<video></video>
, 자동재생을 끄려면 이 속성을 제거해야 합니다. 🎜🎜🎜모든 데이터를 비교한 후에는 하위 노드를 비교할 차례입니다. 먼저 현재 vnode가 텍스트 노드인지 여부를 결정합니다. 텍스트 노드인 경우 하위 노드의 비교를 고려할 필요가 없습니다. 🎜🎜🎜🎜 이전 노드와 새 노드 모두 하위 노드를 갖고 있으면 하위 노드 비교를 입력합니다(diff 알고리즘). 🎜🎜🎜🎜새 노드에는 하위 노드가 있지만 이전 노드에는 없으면 루프에서 dom 노드를 만듭니다. 🎜새 노드에는 하위 노드가 없지만 이전 노드에는 하위 노드가 있습니다. 그런 다음 루프에서 dom 노드를 삭제합니다. 🎜🎜🎜🎜후자의 두 경우는 상대적으로 간단합니다. 첫 번째 경우인 🎜하위 노드 비교🎜를 직접 분석합니다. 🎜🎜diff 알고리즘🎜🎜하위 노드 비교 부분에는 코드가 많습니다. 먼저 원리에 대해 이야기한 후 코드를 게시하겠습니다. 먼저 하위 노드를 비교하는 그림을 살펴보겠습니다. 🎜🎜🎜🎜그림의
oldCh
및newCh
는 각각 이전 및 새 하위 노드 배열을 나타냅니다. 이들은 자체 헤드 및 테일 포인터oldStartIdx
,oldEndIdx
,newStartIdx
,newEndIdx
, 배열은 vnode를 저장하므로 이해하기 쉽도록 a, b, c, d 등으로 대체됩니다. 다양한 유형의 레이블(p,span,p의 vnode 객체)을 나타냅니다.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
oldStartIdx > oldEndIdx || newStartIdx > newEndIdx
). 유통 과정을 대략적으로 살펴보세요. 🎜🎜키가 없는 상황을 먼저 살펴보고, 좀 더 명확하게 볼 수 있도록 애니메이션을 넣어보겠습니다! C 🎜 🎜그림을 읽고 나면 diff 알고리즘의 본질을 더 잘 이해하게 될 것이라고 믿습니다. 전체 과정은 비교적 간단합니다. 위 그림에서는 각 상황에 대해 총 6개의 사이클이 입력되었습니다. 하나씩 설명하겠습니다. 🎜
- 🎜첫 번째 단계는 직접 비교입니다. . 유사하면 이전 헤드 포인터와 새 헤드 포인터가 뒤로 이동하고(예:
oldStartIdx++
&&newStartIdx++
) 실제 DOM은 변경되지 않고 그대로 유지되며 다음 주기로 들어갑니다. 유사하지 않으면 두 번째 단계로 들어갑니다. 🎜- 🎜두 번째 단계는 꼬리-꼬리 비교입니다. 유사하면 이전 끝 포인터와 새 끝 포인터가 앞으로 이동하고(예:
oldEndIdx--
&&newEndIdx--
) 실제 DOM은 변경되지 않고 그대로 유지되며 다음 주기 유사하지 않으면 첫 번째 단계가 입력됩니다. 🎜- 🎜세 번째 단계는 직접 비교입니다. 유사하다면 이전 헤드 포인터는 뒤로 이동하고 새 테일 포인터는 앞으로 이동합니다(예:
oldStartIdx++
&&newEndIdx--
). 확인되지 않은 dom 시퀀스의 헤드가 이동됩니다. 끝까지 가면 다음 단계로 들어갑니다. 유사하지 않으면 4단계로 이동합니다. 🎜- 🎜4단계 꼬리와 머리를 비교하세요. 유사하면 이전 꼬리 포인터는 앞으로 이동하고 새 헤드 포인터는 뒤로 이동합니다(예:
oldEndIdx--
&&newStartIdx++
). 시퀀스가 처음으로 이동하고 다음 시간이 유사하지 않으면 5단계로 이동합니다. 🎜- 🎜5단계: 노드에 키가 있고 이전 하위 노드 배열에서 동일한 Vnode가 발견되면(태그와 키가 모두 일관됨) 해당 DOM을 현재 실제 DOM의 헤드로 이동합니다. 시퀀스, 새 헤드 포인터 뒤로 이동(예:
newStartIdx++
). 그렇지 않으면 vnode(vnode[newStartIdx].elm
)에 해당하는 dom이 현재 포인터의 헤드에 삽입됩니다. 실제 DOM 시퀀스이며 새 헤드 포인터가 뒤로 이동합니다(즉,newStartIdx++
). 🎜🎜그러나 루프를 종료한 후 고려해야 할 두 가지 상황이 있습니다: 🎜
- 🎜처음은 비슷합니다(둘 다 비슷합니다).
a
), dom은 변경되지 않고 이전 헤드 포인터와 새 헤드 포인터가 뒤로 이동됩니다.a
노드가 확인된 후 실제 dom 시퀀스는a,b,c,d,e,f
이고 확인되지 않은 dom 시퀀스는b입니다. ,c,d ,e,f
;🎜- 🎜두 번째로 tail과 tail이 유사할 때(둘 다
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
code>;🎜- 🎜5번째는 유사하지 않으며 확인되지 않은 dom 시퀀스의 헤드에 직접 삽입됩니다.
g
노드가 삽입된 후 실제 dom 시퀀스는a,e,g,c,d,b,f
이고 확인되지 않은 dom 시퀀스는입니다. >c,d
;🎜- 🎜6번째 시간은 유사하지 않으며 확인되지 않은 dom 시퀀스의 헤드에 직접 삽입됩니다.
h
노드가 삽입된 후 실제 dom 시퀀스는a,e,g,h,c,d,b,f
이고 확인되지 않은 dom 시퀀스는 다음과 같습니다.c ,d
;🎜
- 🎜New bytes 포인트 배열(newCh)이 순회됩니다(
newStartIdx > newEndIdx
). 그런 다음 중복된 이전 DOM(oldStartIdx -> oldEndIdx
)을 모두 삭제해야 합니다. 위의 예에서는c,d
; >🎜New 바이트 포인트 배열(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의 가상 돔 비교 원리 소개(예제 설명)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!