1.Vue的diff算法是什么?
diff算法是一种通过同层的树节点比较的高效算法。其有两个特点:
- 比较只会在同层级进行,不会跨层级比较
- 在diff比较的过程中,循环从两边向中间比较
diff算法在很多场景下都有应用,在vue中,作用于虚拟dom渲染成真实dom的新旧VNode节点比较
2.Vue的diff算法的比较方式是什么?
diff算法的整体策略为:深度优先,同层比较。
- 只会在同层级进行,不会跨层级比较
- 在比较过程中,循环会从两边向中间收拢
下面举个vue通过diff算法更新的例子:
新旧节点如下图所示:
第一次循环之后发现旧节点D(old - endIndex)和新节点D(new - startIndex)相同,直接复用旧节点D作为diff后的第一个真实节点,同时旧节点的endIndex往前移动到C,新节点的startIndex移动到C
第二次循环,发现此时旧节点的endIndex对应的节点C与新节点的startIndex对应的节点C相同,故此进行和第一次循环相同的工作
第三次循环发现新节点E未出现在旧节点中,只能直接创建真实节点E插入到C节点之后,然后将新节点的startIndex移动到下一个节点A上
第四次循环发现旧节点的startIndex对应的节点值A和新节点startIndex对应的节点值A相同,故将A节点移动到真实节点E之后。此时同时将旧节点的startIndex后移一位,新节点的startIndex后移一位
第五次循环发现旧节点的startIndex对应的节点B与新节点startIndex对应的节点B相同,所以将节点B移动到真实节点A之后,同时新旧节点的startIndex都后移一位
第六次循环发现旧节点的startIndex的值大于endIndex值,所以就需要创建新节点中所有startIndex到endIndex的节点,放在真实节点B之后
3.Vue的diff算法的原理分析
在Vue中当数据发生变化时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者就会调用patch给真实DOM打补丁,更新相应的视图
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| function patch(oldVnode, vnode, hydrating, removeOnly) { 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) if (!isRealElement && sameVnode(oldVnode, vnode)) { patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly) } else { if (isRealElement) {
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) { oldVnode.removeAttribute(SSR_ATTR) hydrating = true } if (isTrue(hydrating)) { if (hydrate(oldVnode, vnode, insertedVnodeQueue)) { invokeInsertHook(vnode, insertedVnodeQueue, true) return oldVnode } } oldVnode = emptyNodeAt(oldVnode) } return vnode.elm } } }
|
patch函数前两个参数为oldVNode和VNode,分别代表新的节点和之前的旧节点,函数中主要做了四个判断:
1️⃣ 没有新节点,直接触发旧节点的destory钩子
2️⃣ 没有旧节点,说明是页面刚开始初始化的时候,此时,根本不需要比较,直接调用createElm全部新建
3️⃣ 旧节点和新节点一样:通过sameVnode判断节点是否一样,一样直接调用patchVnode去处理这两个节点
4️⃣ 旧节点和新节点自身不一样:当两个节点不一样时,直接创建新节点,删除旧节点
接下来,谈一谈patchVnode部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74
| function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) { if (oldVnode === vnode) { return }
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 } 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 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)) { if (isDef(oldCh) && isDef(ch)) { if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) { if (isDef(oldVnode.text)) nodeOps.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)) { nodeOps.setTextContent(elm, '') }
} else if (oldVnode.text !== vnode.text) { nodeOps.setTextContent(elm, vnode.text) } if (isDef(data)) { if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode) } }
|
patchVnode主要做以下4个判断:
1️⃣ 新节点是否为文本节点,如果是,直接更新dom的文本内容为新节点的文本内容
2️⃣ 新节点和旧节点如果都有子节点,则调用updateChildren函数处理比较更新子节点
3️⃣ 只有新节点有子节点,旧节点没有,不需要比较,所有子节点都是全新的,直接全部新建(创建出新的DOM,添加进老节点里作为子节点)
4️⃣ 只有旧节点有子节点而新节点没有子节点,要做的就是将所有的旧节点的子节点全部删除
小结
- 当Vue中的数据发生变化时,set方法会调用Dep.notify通知所有订阅者Watcher,订阅者会调用patch函数给真实DOM打补丁
- 在patch函数中通过isSameVnode方法进行判断新旧节点是否相同,相同则调用patchVnode方法
- patchVnode方法主要做了以下操作:
-
- 如果新节点均为文本节点,且只有文本内容不同,直接更新旧节点的文本内容即可
- 如果旧节点有子节点而新节点没有子节点,将旧节点的所有子节点删除就OK了
- 如果新节点有子节点而旧节点没有子节点,则创建新节点的子节点添加到旧节点里作为子节点
- 新旧节点都有子节点,调用updateChildren函数比较子节点
- updateChildren函数主要做以下操作:
-
- 新旧头尾指针进行比较,循环向中间靠拢,根据情况调用patchVnode进行patch工作、调用createElm创建新节点等