Virtual dom
과 diff
알고리즘은 vue
학습 과정에서 어려운 점이자 꼭 알아야 할 지식 포인트이기도 합니다 면접에서 마스터하세요. 이 둘은 서로를 보완하며 vue
프레임워크의 핵심입니다. 오늘은 vue2
의 virtual dom
과 diff
알고리즘을 요약해 보겠습니다. (학습 영상 공유: vue 영상 튜토리얼) 虚拟dom
和diff
算法是vue
学习过程中的一个难点,也是面试中必须掌握的一个知识点。这两者相辅相成,是vue
框架的核心。今天我们再来总结下vue2
中的虚拟dom
和 diff
算法。(学习视频分享:vue视频教程)
我们知道,render function
会被转化成 VNode
。VNode
其实就是一棵以 JavaScript
对象作为基础的树,用对象属性来描述节点,实际上它只是一层对真实 DOM
的抽象。最终可以通过一系列操作使这棵树映射到真实环境上。
比如有如下template
<template> <span class="demo" v-show="isShow"> This is a span. </span> </template>
它换成 VNode
以后大概就是下面这个样子
{ tag: "span", data: { /* 指令集合数组 */ directives: [ { /* v-show指令 */ rawName: "v-show", expression: "isShow", name: "show", value: true, }, ], /* 静态class */ staticClass: "demo", }, text: undefined, children: [ /* 子节点是一个文本VNode节点 */ { tag: undefined, data: undefined, text: "This is a span.", children: undefined, }, ], };
总的来说,VNode
就是一个 JavaScript
对象。这个JavaScript
对象能完整地表示出真实DOM
。
笔者认为有两点原因
由于 Virtual DOM
是以 JavaScript
对象为基础而不依赖真实平台环境,所以使它具有了跨平台的能力,比如说浏览器平台、Weex、Node 等。
减少操作DOM
,任何页面的变化,都只使用VNode
进行操作对比,只需要在最后一次进行挂载更新DOM
,避免了频繁操作DOM
,减少了浏览器的回流和重绘从而提高页面性能。
下面我们来看看组件更新所涉及到的diff算法
。
前面我们讲依赖收集的时候有说到,渲染watcher
传递给Watcher
的get
方法其实是updateComponent
方法。
updateComponent = () => { vm._update(vm._render(), hydrating) } new Watcher(vm, updateComponent, noop, { before () { if (vm._isMounted) { callHook(vm, 'beforeUpdate') } } }, true /* isRenderWatcher */)
所以组件在响应式数据发生变化的时候会再次触发该方法,接下来我们来详细分析一下updateComponent
里面的_update
方法。
_update
在_update
方法中做了初始渲染和更新的区分,虽然都是调用__patch__
方法,但是传递的参数不一样。
// src/core/instance/lifecycle.js Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { const vm: Component = this const prevEl = vm.$el const prevVnode = vm._vnode vm._vnode = vnode // 初次渲染没有 prevVnode,组件更新才会有 if (!prevVnode) { // 初次渲染 vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */) } else { // 更新 vm.$el = vm.__patch__(prevVnode, vnode) } // ... }
下面我们再来看看__patch__
方法
__patch__
patch
方法接收四个参数,由于初始渲染的时候oldVnode
为vm.$el
是null
,所以初始渲染是没有oldVnode
。
// src/core/vdom/patch.js return function patch (oldVnode, vnode, hydrating, removeOnly) { // 新节点不存在,只有oldVnode就直接销毁,然后返回 if (isUndef(vnode)) { if (isDef(oldVnode)) invokeDestroyHook(oldVnode) return } let isInitialPatch = false const insertedVnodeQueue = [] // 没有老节点,直接创建,也就是初始渲染 if (isUndef(oldVnode)) { isInitialPatch = true createElm(vnode, insertedVnodeQueue) } else { const isRealElement = isDef(oldVnode.nodeType) // 不是真实dom,并且是相同节点走patch if (!isRealElement && sameVnode(oldVnode, vnode)) { // 这里才会涉及到diff算法 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) { // ... } // replacing existing element const oldElm = oldVnode.elm const parentElm = nodeOps.parentNode(oldElm) // 1.创建一个新节点 createElm( vnode, insertedVnodeQueue, // extremely rare edge case: do not insert if old element is in a // leaving transition. Only happens when combining transition + // keep-alive + HOCs. (#4590) oldElm._leaveCb ? null : parentElm, nodeOps.nextSibling(oldElm) ) // 2.更新父节点占位符 if (isDef(vnode.parent)) { let ancestor = vnode.parent const patchable = isPatchable(vnode) while (ancestor) { for (let i = 0; i < cbs.destroy.length; ++i) { cbs.destroy[i](ancestor) } ancestor.elm = vnode.elm if (patchable) { for (let i = 0; i < cbs.create.length; ++i) { cbs.create[i](emptyNode, ancestor) } const insert = ancestor.data.hook.insert if (insert.merged) { // start at index 1 to avoid re-invoking component mounted hook for (let i = 1; i < insert.fns.length; i++) { insert.fns[i]() } } } else { registerRef(ancestor) } ancestor = ancestor.parent } } // 3.删除老节点 if (isDef(parentElm)) { removeVnodes([oldVnode], 0, 0) } else if (isDef(oldVnode.tag)) { invokeDestroyHook(oldVnode) } } } //触发插入钩子 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch) return vnode.elm }
patch
方法大概流程如下:
没有新节点只有老节点直接删除老节点。
只有新节点没有老节点直接添加新节点。
既有新节点又有老节点则判断是不是相同节点,相同则进入pathVnode
。patchVnode
我们后面会重点分析。
既有新节点又有老节点则判断是不是相同节点,不相同则直接删除老节点添加新节点。
我们再来看看它是怎么判断是同一个节点的。
// src/core/vdom/patch.js function sameVnode (a, b) { return ( a.key === b.key && a.asyncFactory === b.asyncFactory && ( ( a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) || ( isTrue(a.isAsyncPlaceholder) && isUndef(b.asyncFactory.error) ) ) ) } function sameInputType (a, b) { if (a.tag !== 'input') return true let i const typeA = isDef(i = a.data) && isDef(i = i.attrs) && i.type const typeB = isDef(i = b.data) && isDef(i = i.attrs) && i.type return typeA === typeB || isTextInputType(typeA) && isTextInputType(typeB) }
判断两个VNode
节点是否是同一个节点,需要同时满足以下条件
key
相同
都有异步组件工厂函数
tag
(当前节点的标签名)相同
isComment
是否同为注释节点
是否data
(当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型)
当标签是<input>
的时候,type
必须相同
当两个VNode
的tag、key、isComment
都相同,并且同时定义或未定义data
的时候,且如果标签为input则type
必须相同。这时候这两个VNode
则算sameVnode
,可以直接进行patchVnode
操作。
下面我们再来详细分析下patchVnode
方法。
// src/core/vdom/patch.js function patchVnode ( oldVnode, vnode, insertedVnodeQueue, ownerArray, index, removeOnly ) { // 两个vnode相同则直接返回 if (oldVnode === vnode) { return } if (isDef(vnode.elm) && isDef(ownerArray)) { // clone reused vnode vnode = ownerArray[index] = cloneVNode(vnode) } const elm = vnode.elm = oldVnode.elm if (isTrue(oldVnode.isAsyncPlaceholder)) { if (isDef(vnode.asyncFactory.resolved)) { hydrate(oldVnode.elm, vnode, insertedVnodeQueue) } else { vnode.isAsyncPlaceholder = true } return } /* 如果新旧VNode都是静态的,同时它们的key相同(代表同一节点), 并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次), 那么只需要替换componentInstance即可。 */ if (isTrue(vnode.isStatic) && isTrue(oldVnode.isStatic) && vnode.key === oldVnode.key && (isTrue(vnode.isCloned) || isTrue(vnode.isOnce)) ) { vnode.componentInstance = oldVnode.componentInstance return } let i const data = vnode.data /*调用prepatch钩子*/ if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) { i(oldVnode, vnode) } // 获取新老虚拟节点的子节点 const oldCh = oldVnode.children const ch = vnode.children if (isDef(data) && isPatchable(vnode)) { for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode) if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode) } // 新节点不是文本节点 if (isUndef(vnode.text)) { /*新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren*/ if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly) /*如果只有新节点有子节点,先清空elm文本内容,然后为当前DOM节点加入子节点。*/ } else if (isDef(ch)) { if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(ch) } if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '') addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue) /*如果只有老节点有子节点,则移除elm所有子节点*/ } else if (isDef(oldCh)) { removeVnodes(oldCh, 0, oldCh.length - 1) /*当新老节点都无子节点的时候,因为这个逻辑中新节点text不存在,所以直接去除ele的文本*/ } else if (isDef(oldVnode.text)) { nodeOps.setTextContent(elm, '') } // 新节点是文本节点,如果文本不一样就设置新的文本 } else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } /*调用postpatch钩子*/ if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
patchVnode
렌더링 함수
가 VNode</code로 변환된다는 것을 알고 있습니다. >. <code>VNode
는 실제로 JavaScript
객체를 기반으로 하는 트리입니다. 객체 속성은 실제로 노드를 설명하는 데 사용됩니다. 추출. 마지막으로, 이 트리는 일련의 작업을 통해 실제 환경에 매핑될 수 있습니다. 예를 들어 다음과 같은 템플릿
이 있습니다🎜function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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, vnodeToMove, refElm const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] // 老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位。 } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作。并将 oldEndVnode 与 newEndVnode 向前移动一位。 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode 与 newEndVnode 符合 sameVnode 的时候, // 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。 // 然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // oldEndVnode 与 newStartVnode 符合 sameVnode 时, // 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。 // oldEndIdx 向前移动一位,newStartIdx 向后移动一位。 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // createKeyToOldIdx 的作用是产生 key 与 index 索引对应的一个 map 表 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] // 如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined // 同时将 newStartVnode.elm 插入到 oldStartVnode.elm 的前面 if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // 当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的, // 需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多, // 将这些无用的老节点通过 removeVnodes 批量删除即可。 } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
VNode
로 바꾸면 아마도 다음과 같을 것입니다🎜rrreee🎜일반적으로 VNode
는 JavaScript
개체입니다. 이 JavaScript
개체는 실제 DOM
을 완전히 나타낼 수 있습니다. 🎜Virtual DOM
은 JavaScript
개체를 기반으로 하고 실제 플랫폼 환경에 의존하지 않으므로 크로스 플랫폼 기능(예: 브라우저 플랫폼, Weex, Node 등) 🎜DOM
페이지 변경 시 작업 비교를 위해 VNode
만 사용하고 마지막으로 마운트하고 업데이트하면 됩니다< code>DOM은 DOM
의 빈번한 작업을 방지하고 브라우저 리플로우 및 다시 그리기를 줄여 페이지 성능을 향상합니다. 🎜diff 알고리즘
이 포함되었습니다. 🎜🎜앞서 종속성 수집에 대해 이야기할 때 언급했듯이 렌더링 감시자
가 Watcher
에 전달한 get
메서드는 실제로 updateComponent</ 코드코드>방법. 🎜rrreee🎜그래서 구성 요소는 반응형 데이터가 변경되면 이 메서드를 다시 트리거합니다. 다음으로 <code>updateComponent
의 _update
메서드를 자세히 분석해 보겠습니다. 🎜_update
_update
메서드의 초기 렌더링 및 업데이트 차이점 __patch__
메서드가 호출되더라도 전달되는 매개변수가 다르다는 것입니다. 🎜rrreee🎜 __patch__
메소드를 다시 살펴보겠습니다🎜__patch__
patch
메서드는 4개의 매개변수를 받습니다. 초기 렌더링 중에는 oldVnode
가 vm.$el
이므로 null</code입니다. >이므로 초기 렌더링에는 <code>oldVnode
가 없습니다. 🎜rrreee🎜patch
이 방법의 대략적인 프로세스는 다음과 같습니다. 🎜pathVnode
를 입력하세요. patchVnode
나중에 분석에 집중하겠습니다. 🎜VNode
노드가 동일한 노드인지 확인하려면 다음 조건을 동시에 충족해야 합니다🎜key
동일🎜tag
( 현재 노드)는 동일합니다🎜isComment
는 둘 다 주석 노드입니다🎜data
(객체)인지 여부 현재 노드에 해당하는 특정 데이터 정보가 포함되어 있으며 VNodeData 유형입니다)🎜<input>
인 경우 유형</code > 동일해야 합니다🎜</li></ul> 🎜두 <code>VNode
의 tag, key, isComment
가 동일하고 data</code code>는 동시에 정의되거나 정의되지 않으며, 레이블이 < code>입력 유형
인 경우 동일해야 합니다. 이때 이 두 VNode
는 sameVnode
로 간주되어 patchVnode
작업을 직접 수행할 수 있습니다. 🎜patchVnode
메소드를 자세히 분석해 보겠습니다. 🎜rrreee🎜patchVnode
메소드의 대략적인 프로세스는 다음과 같습니다.🎜🎜1. 이전 노드와 새 노드가 동일하면 직접 반환합니다. 🎜2.如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换componentInstance即可。
3.新节点不是文本节点,新老节点均有children
子节点,则对子节点进行diff
操作,调用updateChildren
,这个updateChildren
是diff算法
的核心,后面我们会重点说。
4.新节点不是文本节点,如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
5.新节点不是文本节点,当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
6.新节点不是文本节点,并且新老节点都无子节点的时候,只需要将老节点文本清空。
7.新节点是文本节点,并且新老节点文本不一样,则进行文本的替换。
updateChildren
是diff
算法的核心,下面我们来重点分析。
这两张图代表旧的VNode与新VNode进行patch的过程,他们只是在同层级的VNode之间进行比较得到变化(相同颜色的方块代表互相进行比较的VNode节点),然后修改变化的视图,所以十分高效。所以Diff算法是:深度优先算法
。 时间复杂度:O(n)
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) { 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, vnodeToMove, refElm const canMove = !removeOnly if (process.env.NODE_ENV !== 'production') { checkDuplicateKeys(newCh) } while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) { if (isUndef(oldStartVnode)) { oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left } else if (isUndef(oldEndVnode)) { oldEndVnode = oldCh[--oldEndIdx] // 老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode,同时 oldStartIdx 与 newStartIdx 向后移动一位。 } else if (sameVnode(oldStartVnode, newStartVnode)) { patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldStartVnode = oldCh[++oldStartIdx] newStartVnode = newCh[++newStartIdx] // 两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode 操作。并将 oldEndVnode 与 newEndVnode 向前移动一位。 } else if (sameVnode(oldEndVnode, newEndVnode)) { patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) oldEndVnode = oldCh[--oldEndIdx] newEndVnode = newCh[--newEndIdx] // oldStartVnode 与 newEndVnode 符合 sameVnode 的时候, // 将 oldStartVnode.elm 这个节点直接移动到 oldEndVnode.elm 这个节点的后面即可。 // 然后 oldStartIdx 向后移动一位,newEndIdx 向前移动一位。 } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx) canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm)) oldStartVnode = oldCh[++oldStartIdx] newEndVnode = newCh[--newEndIdx] // oldEndVnode 与 newStartVnode 符合 sameVnode 时, // 将 oldEndVnode.elm 插入到 oldStartVnode.elm 前面。 // oldEndIdx 向前移动一位,newStartIdx 向后移动一位。 } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm) oldEndVnode = oldCh[--oldEndIdx] newStartVnode = newCh[++newStartIdx] } else { // createKeyToOldIdx 的作用是产生 key 与 index 索引对应的一个 map 表 if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) idxInOld = isDef(newStartVnode.key) ? oldKeyToIdx[newStartVnode.key] : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx) // 如果没有找到相同的节点,则通过 createElm 创建一个新节点,并将 newStartIdx 向后移动一位。 if (isUndef(idxInOld)) { // New element createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else { vnodeToMove = oldCh[idxInOld] // 如果找到了节点,同时它符合 sameVnode,则将这两个节点进行 patchVnode,将该位置的老节点赋值 undefined // 同时将 newStartVnode.elm 插入到 oldStartVnode.elm 的前面 if (sameVnode(vnodeToMove, newStartVnode)) { patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) oldCh[idxInOld] = undefined canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm) } else { // 如果不符合 sameVnode,只能创建一个新节点插入到 parentElm 的子节点中,newStartIdx 往后移动一位。 createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } } newStartVnode = newCh[++newStartIdx] } } // 当 while 循环结束以后,如果 oldStartIdx > oldEndIdx,说明老节点比对完了,但是新节点还有多的, // 需要将新节点插入到真实 DOM 中去,调用 addVnodes 将这些节点插入即可。 if (oldStartIdx > oldEndIdx) { refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue) // 如果满足 newStartIdx > newEndIdx 条件,说明新节点比对完了,老节点还有多, // 将这些无用的老节点通过 removeVnodes 批量删除即可。 } else if (newStartIdx > newEndIdx) { removeVnodes(oldCh, oldStartIdx, oldEndIdx) } }
vue2
的diff
算法采用的是双端比较
,所谓双端比较
就是新列表和旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。
我们首先来看看首尾对比的四种情况。
使用旧列表的头一个节点oldStartNode
与新列表的头一个节点newStartNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的头一个节点oldStartNode
与新列表的最后一个节点newEndNode
对比
使用旧列表的最后一个节点oldEndNode
与新列表的头一个节点newStartNode
对比
首先是 oldStartVnode
与 newStartVnode
符合 sameVnode
时,说明老 VNode 节点的头部与新 VNode 节点的头部是相同的 VNode 节点,直接进行 patchVnode
,同时 oldStartIdx
与 newStartIdx
向后移动一位。
其次是 oldEndVnode
与 newEndVnode
符合 sameVnode
,也就是两个 VNode 的结尾是相同的 VNode,同样进行 patchVnode
操作并将 oldEndVnode
与 newEndVnode
向前移动一位。
接下来是两种交叉的情况。
先是 oldStartVnode
与 newEndVnode
符合 sameVnode
的时候,也就是老 VNode 节点的头部与新 VNode 节点的尾部是同一节点的时候,将 oldStartVnode.elm
这个节点直接移动到 oldEndVnode.elm
这个节点的后面即可。然后 oldStartIdx
向后移动一位,newEndIdx
向前移动一位。
同理,oldEndVnode
与 newStartVnode
符合 sameVnode
时,也就是老 VNode 节点的尾部与新 VNode 节点的头部是同一节点的时候,将 oldEndVnode.elm
插入到 oldStartVnode.elm
前面。同样的,oldEndIdx
向前移动一位,newStartIdx
向后移动一位。
마지막으로 위의 상황 중 어느 것도 충족되지 않으면 이 상황을 어떻게 처리해야 할까요?
그것이 바로 검색 및 비교입니다.
먼저 createKeyToOldIdx
메소드를 사용하여 oldVnode
의 key
와 index에 해당하는 <code>맵
을 생성합니다. 인덱스. createKeyToOldIdx
方法生成oldVnode
的key
与 index
索引对应的一个 map
表。
然后我们根据newStartVnode.key
,可以快速地从 oldKeyToIdx
(createKeyToOldIdx
的返回值)中获取相同 key
的节点的索引 idxInOld
,然后找到相同的节点。
这里又分三种情况
如果没有找到相同的节点,则通过 createElm
创建一个新节点,并将 newStartIdx
向后移动一位。
如果找到了节点,同时它符合 sameVnode
,则将这两个节点进行 patchVnode
,将该位置的老节点赋值 undefined
(之后如果还有新节点与该节点key相同可以检测出来提示已有重复的 key ),同时将 newStartVnode.elm
插入到 oldStartVnode.elm
的前面。同理,newStartIdx
往后移动一位。
如果不符合 sameVnode
,只能创建一个新节点插入到 parentElm
的子节点中,newStartIdx
往后移动一位。
最后一步就很容易啦,当 while
循环结束以后,如果 oldStartIdx > oldEndIdx
,说明老节点比对完了,但是新节点还有多的,需要将新节点插入到真实 DOM 中去,调用 addVnodes
将这些节点插入即可。
同理,如果满足 newStartIdx > newEndIdx
条件,说明新节点比对完了,老节点还有多,将这些无用的老节点通过 removeVnodes
批量删除即可。
Diff算法是一种对比算法。对比两者是旧虚拟DOM和新虚拟DOM
,对比出是哪个虚拟节点
更改了,找出这个虚拟节点
,并只更新这个虚拟节点所对应的真实节点
,而不用更新其他数据没发生改变的节点,实现精准
地更新真实DOM,进而提高效率和性能
。
精准
主要体现在,diff
算法首先就是找到可复用的节点,然后移动到正确的位置。当元素没有找到的话再来创建新节点。
key
是 Vue
中 vnode
的唯一标记,通过这个 key
,diff
操作可以更准确、更快速。
key
就不是就地复用了,在 sameNode
函数 a.key === b.key
对比中可以避免就地复用的情况。所以会更加准确。key
的唯一性生成 map
对象来获取对应节点,比遍历方式更快。当我们的列表只涉及到 展示,不涉及到排序、删除、添加的时候使用index
作为key
是没什么问题的。因为此时的index
在每个元素上是唯一的。
但是如果涉及到排序、删除、添加的时候就不能再使用index
作为key
了,因为每个元素key
不再唯一了。不唯一的key
,对diff
newStartVnode.key를 기반으로 <code>oldKeyToIdx
(createKeyToOldIdx
의 반환 값)에서 동일한 key
를 빠르게 얻을 수 있습니다. code> 노드 code>의 인덱스는 idxInOld
이고, 동일한 노드가 검색됩니다. 🎜🎜여기에는 세 가지 상황이 있습니다🎜createElm
을 통해 새 노드를 생성하고, 그리고 newStartIdx
를 한 자리 뒤로 이동합니다. 🎜sameVnode
를 준수하는 경우 이 두 노드에서 patchVnode
를 수행하고 해당 위치에 이전 노드를 할당합니다undefinenewStartVnode.elm을 삽입합니다.
를
앞의 oldStartVnode.elm에 추가합니다. 같은 방식으로 newStartIdx
는 한 위치 뒤로 이동합니다. 🎜🎜🎜sameVnode
와 일치하지 않으면 새 노드를 생성하여 삽입만 가능합니다. parentElm의 하위 노드 중 <code>newStartIdx
는 한 위치 뒤로 이동합니다. 🎜
while
루프가 끝나면 마지막 단계는 매우 쉽습니다. oldStartIdx > oldEndIdx는 이전 노드 비교가 완료되었지만 아직 새 노드가 많이 있음을 나타냅니다. 삽입하려면 addVnodes
를 호출하세요. 이 노드. 🎜🎜🎜🎜동일 newStartIdx > newEndIdx
의 조건이 충족되면 새 노드의 비교가 완료되었으며 여전히 오래된 노드가 많이 있다는 의미입니다. removeVnodes를 통해 일괄 삭제할 수 있습니다.
. 🎜🎜🎜이전 가상 DOM과 새 가상 DOM
을 비교하고, 어떤 가상 노드
가 변경되었는지 비교하고, 이 가상 노드
를 찾고, 만 업데이트합니다. >실제 노드
이 가상 노드에 해당하는 데이터가 변경되지 않은 다른 노드를 업데이트하는 대신 실제 DOM을 정확하게
업데이트하여 효율성과 성능을 향상
합니다. >. 🎜🎜정확도
는 주로 diff
알고리즘이 먼저 🎜재사용 가능한 노드🎜를 찾은 다음 🎜올바른 위치로 이동🎜한다는 사실에 반영됩니다. 요소를 찾을 수 없으면 새 노드를 만듭니다. 🎜key
는 Vue
에서 vnode
의 유일한 태그입니다. 이 key
를 통해 > diff
작업이 더 정확하고 빨라질 수 있습니다. 🎜key
를 사용하면 sameNode
함수에서 a.key === b.key로 재사용되지 않기 때문입니다.
이렇게 하면 비교 중에 내부 재사용을 피할 수 있습니다. 그래서 더 정확할 것입니다. 키
의 고유성을 사용하여 맵
개체를 생성하여 해당 노드를 획득하는데, 이는 순회 방법보다 빠릅니다. index
를 key
로 사용하는데 문제가 없습니다. 현재 색인
은 각 요소마다 고유하기 때문입니다. 🎜🎜그러나 정렬, 삭제 또는 추가가 포함된 경우에는 더 이상 index
를 key
로 사용할 수 없습니다. 각 요소의 key
가 더 이상 단 하나. 고유하지 않은 키
는 diff
알고리즘에 도움이 되지 않습니다. 🎜(학습 영상 공유: 웹 프론트엔드 개발, 기본 프로그래밍 영상)
위 내용은 vue2의 VNode 및 diff 알고리즘에 대한 심층적인 이해의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!