이 글은 vue.js의 diff 알고리즘에 대한 자세한 이해를 제공합니다. 도움이 필요한 친구들이 모두 참고할 수 있기를 바랍니다.
머리말
diff에 대한 매우 상세한 소개를 작성하는 것이 목표여서 글이 좀 깁니다. 또한 이 글을 읽는 친구들이 diff의 모든 면을 이해할 수 있도록 많은 수의 그림과 코드 예제도 사용됩니다.
먼저 몇 가지 사항을 이해해 봅시다...
1. 데이터가 변경되면 vue는 어떻게 노드를 업데이트하나요?
실제 DOM을 렌더링하는 데 드는 비용이 매우 높다는 것을 알아야 합니다. 예를 들어 특정 데이터를 실제 DOM에 직접 렌더링하면 전체 DOM 트리가 다시 그려지고 재배열되는 경우가 있습니다. 전체 DOM을 업데이트하는 대신 수정한 DOM의 작은 부분만 업데이트하는 것이 가능합니까? diff 알고리즘이 우리에게 도움이 될 수 있습니다.
먼저 실제 DOM을 기반으로 가상 DOM을 생성합니다. 가상 DOM의 노드 데이터가 변경되면 새 Vnode가 생성됩니다. 그런 다음 Vnode를 이전 Vnode와 비교하여 차이가 있으면 직접 수정합니다. 실제 DOM에서 on을 선택한 다음 oldVnode의 값을 Vnode로 설정합니다.
diff의 과정은 patch라는 함수를 호출하고, 이전 노드와 새 노드를 비교하고, 비교하면서 실제 DOM을 패치하는 것입니다.
2. 가상 DOM과 실제 DOM의 차이점은 무엇인가요?
Virtual DOM은 실제 DOM 데이터를 추출하고 트리 구조를 객체 형태로 시뮬레이션합니다. 예를 들어 dom은 다음과 같습니다.
<div> <p>123</p> </div>
해당 가상 DOM(의사 코드):
var Vnode = { tag: 'div', children: [ { tag: 'p', text: '123' } ] };
(주의 사항: VNode와 oldVNode는 모두 객체이므로 꼭 기억하세요)
3. diff를 비교하는 방법은 무엇입니까?
diff 알고리즘을 사용하여 이전 노드와 새 노드를 비교할 때 비교는 동일한 수준에서만 수행되며 수준 간 비교는 수행되지 않습니다.
<div> <p>123</p> </div> <div> <span>456</span> </div>
위 코드는 동일한 레이어의 두 div와 두 번째 레이어의 p 및span을 비교하지만 div와 스팬을 비교하지는 않습니다. 다른 곳에서 본 매우 생생한 그림:
diff flow Chart
데이터가 변경되면 set 메소드는 Dep.notify를 호출하여 모든 구독자 Watcher에게 알리고 구독자는 patch Patch를 호출하여 실제 DOM과 해당 뷰를 업데이트합니다.
자세한 분석
patch
패치가 어떻게 패치되는지 살펴보겠습니다(코드의 핵심 부분만 유지됨)
function patch (oldVnode, vnode) { // some code if (sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode) } else { const oEl = oldVnode.el // 当前oldVnode对应的真实元素节点 let parentEle = api.parentNode(oEl) // 父元素 createEle(vnode) // 根据Vnode生成新元素 if (parentEle !== null) { api.insertBefore(parentEle, vnode.el, api.nextSibling(oEl)) // 将新元素添加进父元素 api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点 oldVnode = null } } // some code return vnode }
패치 함수는 새 노드를 나타내는 두 개의 매개변수 oldVnode 및 Vnode를 받습니다. 및 Vnode는 각각 두 노드를 비교할 가치가 있는지 결정합니다. 비교할 가치가 있으면 patchVnode
function sameVnode (a, b) { return ( a.key === b.key && // key值 a.tag === b.tag && // 标签名 a.isComment === b.isComment && // 是否为注释节点 // 是否都定义了data,data包含一些具体信息,例如onclick , style isDef(a.data) === isDef(b.data) && sameInputType(a, b) // 当标签是<input>的时候,type必须相同 ) }
를 실행합니다. 비교할 가치가 없으면 oldVnode를 Vnode
로 교체합니다. , 그런 다음 하위 노드를 자세히 확인하세요. 두 노드가 다르다면 Vnode가 완전히 변경되었다는 의미이며, oldVnode를 직접 교체할 수 있습니다.
두 노드가 서로 다르지만, 자식 노드가 같다면 어떻게 해야 하나요? 잊지 마세요. diff는 레이어별로 비교됩니다. 첫 번째 레이어가 다르면 두 번째 레이어는 깊이 비교되지 않습니다. (이게 단점일까? 같은 자식 노드는 재사용할 수 없다...)
patchVnode두 노드가 비교할 가치가 있다고 판단되면 두 노드에 patchVnode 메소드를 할당하게 된다. 그렇다면 이 방법은 무엇을 하는가?
patchVnode (oldVnode, vnode) { const el = vnode.el = oldVnode.el let i, oldCh = oldVnode.children, ch = vnode.children if (oldVnode === vnode) return if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) { api.setTextContent(el, vnode.text) }else { updateEle(el, vnode, oldVnode) if (oldCh && ch && oldCh !== ch) { updateChildren(el, oldCh, ch) }else if (ch){ createEle(vnode) //create el's children dom }else if (oldCh){ api.removeChildren(el) } } }
이 함수는 다음을 수행합니다.
updateChildren (parentElm, oldCh, newCh) { let oldStartIdx = 0, 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 let idxInOld let elmToMove let before while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (oldStartVnode == null) { // 对于vnode.key的比较,会把oldVnode = null oldStartVnode = oldCh[++oldStartIdx] }else if (oldEndVnode == null) { oldEndVnode = oldCh[--oldEndIdx] }else if (newStartVnode == null) { newStartVnode = newCh[++newStartIdx] }else if (newEndVnode == null) { newEndVnode = newCh[--newEndIdx] }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.el, api.nextSibling(oldEndVnode.el)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] }else if (sameVnode(oldEndVnode, newStartVnode)) { patchVnode(oldEndVnode, newStartVnode) api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] }else { // 使用key时的比较 if (oldKeyToIdx === undefined) { oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表 } idxInOld = oldKeyToIdx[newStartVnode.key] if (!idxInOld) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) newStartVnode = newCh[++newStartIdx] } else { elmToMove = oldCh[idxInOld] if (elmToMove.sel !== newStartVnode.sel) { api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el) }else { patchVnode(elmToMove, newStartVnode) oldCh[idxInOld] = null api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el) } newStartVnode = newCh[++newStartIdx] } } } if (oldStartIdx > oldEndIdx) { before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx) }else if (newStartIdx > newEndIdx) { removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx) } }
먼저 이 함수의 기능에 대해 이야기해 보겠습니다.
Vnode의 하위 노드 Vch와 oldVnode의 하위 노드 oldCh를 추출합니다
oldCh와 vCh는 각각 두 개의 헤드 및 테일 변수 StartIdx와 EndIdx를 가지며, 거기에는 2개가 있습니다. 변수를 서로 비교하는 네 가지 비교 방법이 있습니다. 4가지 비교 중 일치하는 항목이 없으면 키가 설정된 경우 비교 과정에서 변수가 중간으로 이동합니다. 일단 StartIdx>EndIdx는 oldCh와 vCh 중 하나 이상이 있음을 나타냅니다. 통과하면 비교가 종료됩니다.
图解updateChildren
终于来到了这一部分,上面的总结相信很多人也看得一脸懵逼,下面我们好好说道说道。(这都是我自己画的,求推荐好用的画图工具...)
粉红色的部分为oldCh和vCh
我们将它们取出来并分别用s和e指针指向它们的头child和尾child
现在分别对oldS、oldE、S、E两两做sameVnode比较,有四种比较方式,当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,这句话有点绕,打个比方
如果是oldS和E匹配上了,那么真实dom中的第一个节点会移到最后
如果是oldE和S匹配上了,那么真实dom中的最后一个节点会移到最前,匹配上的两个指针向中间移动
如果四种匹配没有一对是成功的,那么遍历oldChild,S挨个和他们匹配,匹配成功就在真实dom中将成功的节点移到最前面,如果依旧没有成功的,那么将S对应的节点插入到dom中对应的oldS位置,oldS和S指针向中间移动。
再配个图
第一步
oldS = a, oldE = d; S = a, E = b;
oldS和S匹配,则将dom中的a节点放到第一个,已经是第一个了就不管了,此时dom的位置为:a b d
第二步
oldS = b, oldE = d; S = c, E = b;
oldS和E匹配,就将原本的b节点移动到最后,因为E是最后一个节点,他们位置要一致,这就是上面说的:当其中两个能匹配上那么真实dom中的相应节点会移到Vnode相应的位置,此时dom的位置为:a d b
第三步
oldS = d, oldE = d; S = c, E = d;
oldE和E匹配,位置不变此时dom的位置为:a d b
第四步
oldS++; oldE--; oldS > oldE;
遍历结束,说明oldCh先遍历完。就将剩余的vCh节点根据自己的的index插入到真实dom中去,此时dom位置为:a c d b
一次模拟完成。
这个匹配过程的结束有两个条件:
oldS > oldE表示oldCh先遍历完,那么就将多余的vCh根据index添加到dom中去(如上图)
S > E表示vCh先遍历完,那么就在真实dom中将区间为[oldS, oldE]的多余节点删掉
下面再举一个例子,可以像上面那样自己试着模拟一下
当这些节点sameVnode成功后就会紧接着执行patchVnode了,可以看一下上面的代码
if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode) }
总结
以上为diff算法的全部过程,放上一张文章开始就发过的总结图,可以试试看着这张图回忆一下diff的过程。
相关推荐:
更多编程相关知识,请访问:编程入门!!
위 내용은 vue.js의 diff 알고리즘에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!