1. 当数据发生变化时,vue是怎么更新节点的?
Vue在初次渲染之后会根据真实DOM生成一颗virtual DOM,在之后的渲染过程中,当virtual DOM某个节点的数据改变后会生成一个新的Vnode,然后Vnode和oldVnode作对比,发现有不一样的地方就直接修改在真实的DOM上,然后使oldVnode的值为Vnode。
diff的过程就是调用名为patch的函数,比较新旧节点,一边比较一边给真实的DOM打补丁。
export function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
const realNode = creatElm(vnode);
oldVnode.parentNode.insertBefore(realNode, oldVnode.nextSibling);
oldVnode.remove();
return realNode
} else {
const el = patchVnode(oldVnode, vnode)
return el;
}
}
patch函数收两个参数,第一个参数表示的是老的节点,它可以是虚拟DOM,也可以是真实DOM,通常情况下,在组件的初次渲染的时候,oldVnode是真实DOM。
对于真实DOM的情况的处理办法也很简单,即:删除老的DOM节点,然后插入新的DOM,新的DOM做了模板解析,同步了vm的数据。
diff算法流程图:
Diff算法的核心在于两个都是虚拟DOM的时候。
function patchVnode(oldVnode, vnode) {
if (!isSameVnode(oldVnode, vnode)) {
// 不是一个节点
const newEl = creatElm(vnode)
oldVnode.el.parentNode.replaceChild(newEl, oldVnode.el)
return newEl
}
// 复用节点
const el = vnode.el = oldVnode.el;
// 处理文本节点
if (!oldVnode.tag) {
if (el.textContent !== vnode.text) {
el.textContent = vnode.text;
}
}
// 对比属性
patchProps(el, vnode.data, oldVnode.data)
const oldChildren = oldVnode.children || [];
const newChildren = vnode.children || [];
if (oldChildren.length && newChildren.length) {
updateChildren(el, oldChildren, newChildren)
} else if (newChildren.length) {
// 新的有 老节点没有
mountChildren(el, newChildren);
} else if (oldChildren.length) {
// 新的有 老的有
el.children.forEach(child => child.remove())
}
return el;
}
首先我们要知道Diff算法的比较方式:在采取diff算法比较新旧节点的时候,比较只会在同层级进行, 不会跨层级比较。
所以在isSameVnode(oldVnode, vnode)为false的时候,就直接删除原来的节点,替换为新的节点。当两个节点的Key和Tag是相同的时候,Vue会让新的节点复用老的节点(这个时候新的虚拟节点上其实还没有el,这样做就省去了创建新的节点的过程),随后比较文本内容,比较完毕之后进行子节点的比较。
function updateChildren(el, oldChildren, newChildren) {
let oldStartIndex = 0;
let newStartIndex = 0;
let oldEndIndex = oldChildren.length - 1;
let newEndIndex = newChildren.length - 1;
let oldStartVnode = oldChildren[oldStartIndex];
let oldEndVnode = oldChildren[oldEndIndex];
let newStartVNode = newChildren[newStartIndex];
let newEndVNode = newChildren[newEndIndex];
function makeKeyByIndex(children) {
const map = new Map();
children.forEach((child, index) => map.set(child.key, index))
return map
}
const map = makeKeyByIndex(oldChildren);
while (newStartIndex <= newEndIndex && oldStartIndex <= oldEndIndex) {
if (!oldStartVnode) {
oldStartVnode = oldChildren[++oldStartIndex]
} else if (!oldEndVnode) {
oldEndVnode = [--oldEndIndex]
} else if (isSameVnode(newStartVNode, oldStartVnode)) {
patch(oldStartVnode, newStartVNode);
oldStartVnode = oldChildren[++oldStartIndex];
newStartVNode = newChildren[++newStartIndex];
} else if (isSameVnode(newEndVNode, oldEndVnode)) {
patch(oldEndVnode, newEndVNode);
oldEndVnode = oldChildren[--oldEndIndex];
newEndVNode = newChildren[--newEndIndex];
} else if (isSameVnode(newStartVNode, oldEndVnode.el)) {
patch(oldEndVnode, newStartVNode);
el.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldChildren[--oldEndIndex];
newStartVNode = newChildren[++newStartIndex];
} else if (isSameVnode(newEndVNode, oldStartVnode)) {
patch(oldStartVnode, newEndVNode);
el.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
newEndVNode = newChildren[--newEndIndex];
oldStartVnode = oldChildren[++oldStartIndex];
} else {
// 乱序比对
const moveChildIndex = map.get(newStartVNode.key);
if (moveChildIndex !== undefined) {
const moveChild = oldChildren[moveChildIndex];
el.insertBefore(moveChild.el, oldStartVnode.el);
oldChildren[moveChildIndex] = null;
patch(moveChild, newStartVNode);
} else {
el.insertBefore(moveChild.el, creatElm(newStartVNode));
}
newStartVNode = newChildren[++newStartIndex];
}
}
if (newStartIndex <= newEndIndex) {
// 新的节点多了
for (let i = newStartIndex; i <= newEndIndex; i++) {
const newChild = creatElm(newChildren[i]);
const anchor = oldEndVnode.el.nextSibling || null;
el.insertBefore(newChild, anchor);
}
}
if (oldStartIndex <= oldEndIndex) {
// 老的节点多了
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
oldChildren[i] && oldChildren[i].el.remove()
}
}
}
子节点的比对是比较困难的地方,vue2的算法diff算法使用的是双指针比对,也就是:oldStartIndex,newStartIndex,oldEndIndex,newEndIndex,分别指向了新头新尾和老头老尾。vue把节点分为了5种情况,分别是新头=老头、新尾=老尾、新头=老尾、新尾=老头以及乱序。
新头=老头、新尾=老尾是比较好理解的,这是比较常规的头头和尾尾比较,当比对成功的时候,就执行让新老节点执行patch方法,递归比较新老节点的子节点或者文本内容,这也就说明Vue 的diff 算法是一种深度优先的算法。
常规比较
新尾和老尾的比较也是大同小异。
交叉比较
当新头=老尾或者新尾=老头的时候
就会进行新节点的头部与老节点的尾部比较:
比对完毕后,把老节点的A插入到老节点中的指向最后元素的指针指向的下一个,然后新头向后移动,老尾向前移动
乱序比对
以上都是比较有规律的情况,还有一种就是乱序比对了,当两个节点不满足上面的规律的时候就会进行乱序比对。
乱序比对的时候,Vue会先为老节点生成一份map映射表,新的节点会在映射表中查找是否有对应的key,有的话就会直接把对应地老节点移动到开头的位置
如果没有,就会根据新的虚拟节点创建真实DOM,插入到老节点头指针指向的节点的前一个
在循环比对完毕之后,Vue会对新老节点进行最后一步的处理:
if (newStartIndex <= newEndIndex) {
// 新的节点多了
for (let i = newStartIndex; i <= newEndIndex; i++) {
const newChild = creatElm(newChildren[i]);
const anchor = oldEndVnode.el.nextSibling || null;
el.insertBefore(newChild, anchor);
}
}
if (oldStartIndex <= oldEndIndex) {
// 老的节点多了
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
oldChildren[i] && oldChildren[i].el.remove()
}
}
也就是删除老节点多余的,添加新节点新增的。
至此Vue2.xDIff算法介绍完毕
参考
详解vue的diff算法 juejin.cn/post/684490…