在更新dom的时候vue并不是实时更新真实dom的变化,而是通过对比oldvnode和newvnode两个虚拟dom来进行最小化的更新视图
更新的时候遵守同级对比,不能跨层,以下是图的示例

具体更新规则如下,依次判断
- oldvnode和newvnode都有文本节点,则用新的文本节点替换旧文本节点
- oldvnode没有子节点,newvnode有子节点,则添加新的子节点
- oldvnode有子节点,newvnode没有子节点吗,则删除旧的子节点
- oldvnode和newvnode都有子节点,则updateChildren()方法
具体patch代码如下:
import createElement from "./createElement"; //创建一个真实的dom
import vnode from "./vnode"; //生成虚拟dom
import updateChildren from "./updateChildren";
export default function(oldVnode, newVnode) {
//判断传入的第一个参数,是DOM节点还是虚拟节点
if(oldVnode.sel == '' || oldVnode.sel == undefined) {
//传入的第一个参数是DOM节点,此时要包装为虚拟节点
oldVnode = vnode(oldVnode.tagName.toLowerCase(), {}, [],undefined,oldVnode)
}
//判断oldVnode和newVnode是不是同一个节点
if(oldVnode.key == newVnode.key && oldVnode.sel == newVnode.sel) {
//判断是不是同一个对象
if(oldVnode == newVnode) return
//判断新vnode有没有text属性
if(newVnode.text !== undefined && (newVnode.children == undefined || newVnode.children.length == 0)) {
//新vnode只有text属性
if(newVnode.text != oldVnode.text) {
//新虚拟节点和老虚拟节点的text不同直接替换掉
oldVnode.elm.innerText = newVnode.text
}
}else {
//新vnode没有text属性,表示有children属性,挂有子节点
if(oldVnode.children !== undefined && oldVnode.children.length > 0) {
//老的有children,复杂情况
updateChildren(oldVnode.elm, oldVnode.children, newVnode.children)
}else {
//老的没有children,新的有children
//清空老的节点内容
oldVnode.elm.innerHTML = ''
//遍历新的vbode的子节点,创建dom上树
for(let i = 0; i < newVnode.children.length; i++) {
let dom = createElement(newVnode.children[i])
oldVnode.elm.appendChild(dom)
}
}
}
}else {
let newVnodeElm = createElement(newVnode) //第二次更新的时候是这个让vnode有了elm属性
//插入到老节点之前
if(oldVnode.elm.parentNode !== undefined && newVnodeElm){
oldVnode.elm.parentNode.insertBefore(newVnodeElm, oldVnode.elm)
}
oldVnode.elm.parentNode.removeChild(oldVnode.elm)
}
}
updateChildren的时候首先遵循四种命中策略
依次是创建四个指针,指向newvnode的children的头节点(新前)、尾节点(新后),oldvnode的children的头节点(旧前)、尾节点(旧后)。
然后根据四个指针循环判断
- 新前与旧前是否命中(标签名相同,key相同),如果命中则进行patch,在patch完成之后,需要移动指针,新前+1,旧前+1,指针靠拢,如果未命中则判断以下
- 新后与旧后是否命中,如果命中则进行patch,在patch完成之后,需要移动指针,新后-1,旧后-1,指针靠拢,未命中则判断以下
- 新后与旧前是否命中,如果命中则进行patch,在patch完成之后,不同于1、2命中方式,需要移动真实的dom,因为dom结构已经发生了变化,接着移动指针,新后-1,旧前+1,指针靠拢,还未命中则继续以下
- 新前与旧后是否命中,如果命中则进行patch,在patch完成之后,需要移动指针,新前+1,旧后-1,指针靠拢,还未命中则继续以下
则需要创建一个map保存oldvode的children的子节点的key和数组位置idx的映射关系,然后找到新前的key对应map的idx,如果没找到则说明是新节点,直接插入到oldStartVnode.elm之前,如果找到则进行patch然后将oldChild队形的idx项置为undefined,用于下次循环的时候略过。
如果循环结束,newStartIdx <= newEndIdx或者oldStartIdx <= oldEndIdx 则添加或删除节点
具体代码如下
import createElement from "./createElement";
import patch from "./patch";
export default function updateChildren(parentElm, oldCh, newCh) {
// 四个指针
// 旧前
let oldStartIdx = 0;
// 新前
let newStartIdx = 0;
// 旧后
let oldEndIdx = oldCh.length - 1;
// 新后
let newEndIdx = newCh.length - 1;
// 指针指向的四个节点
// 旧前节点
let oldStartVnode = oldCh[0];
// 旧后节点
let oldEndVnode = oldCh[oldEndIdx];
// 新前节点
let newStartVnode = newCh[0];
// 新后节点
let newEndVnode = newCh[newEndIdx];
//map映射
let keyMap = null;
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 首先应该不是判断四种命中,而是略过已经加了undefined标记的项
if (oldCh[oldStartIdx] === undefined) {
oldStartVnode = oldCh[++oldStartIdx];
} else if (oldCh[oldEndIdx] === undefined) {
oldEndVnode = oldCh[--oldEndIdx];
} else if (newCh[newStartIdx] === undefined) {
newStartVnode = newCh[++newStartIdx];
} else if (newCh[newEndIdx] === undefined) {
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newStartVnode)) {
// 新前与旧前
console.log("新前与旧前命中");
// 精细化比较两个节点 oldStartVnode现在和newStartVnode一样了
patch(oldStartVnode, newStartVnode);
// 移动指针,改变指针指向的节点,这表示这两个节点都处理(比较)完了
oldStartVnode = oldCh[++oldStartIdx];
newStartVnode = newCh[++newStartIdx];
} else if (checkSameVnode(oldEndVnode, newEndVnode)) {
// 新后与旧后
console.log("新后与旧后命中");
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldStartVnode, newEndVnode)) {
// 新后与旧前
console.log("新后与旧前命中");
patch(oldStartVnode, newEndVnode);
// 当③新后与旧前命中的时候,此时要移动节点。移动 新后(旧前) 指向的这个节点到老节点的 旧后的后面
// 移动节点:只要插入一个已经在DOM树上 的节点,就会被移动
parentElm.insertBefore(oldStartVnode.elm, oldEndVnode.elm.nextSibling);
oldStartVnode = oldCh[++oldStartIdx];
newEndVnode = newCh[--newEndIdx];
} else if (checkSameVnode(oldEndVnode, newStartVnode)) {
// 新前与旧后
console.log("新前与旧后命中");
patch(oldEndVnode, newStartVnode);
// 当④新前与旧后命中的时候,此时要移动节点。移动 新前(旧后) 指向的这个节点到老节点的 旧前的前面
// 移动节点:只要插入一个已经在DOM树上的节点,就会被移动
parentElm.insertBefore(oldEndVnode.elm, oldStartVnode.elm);
oldEndVnode = oldCh[--oldEndIdx];
newStartVnode = newCh[++newStartIdx];
} else {
// 四种都没有匹配到,都没有命中
console.log("四种都没有命中");
// 寻找 keyMap 一个映射对象, 就不用每次都遍历old对象了
if (!keyMap) {
keyMap = {};
// 记录oldVnode中的节点出现的key
// 从oldStartIdx开始到oldEndIdx结束,创建keyMap
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
const key = oldCh[i].key;
if (key !== undefined) {
keyMap[key] = i;
}
}
}
// 寻找当前项(newStartIdx)在keyMap中映射的序号
const idxInOld = keyMap[newStartVnode.key];
if (idxInOld === undefined) {
// 如果 idxInOld 是 undefined 说明是全新的项,要插入
// 被加入的项(就是newStartVnode这项)现不是真正的DOM节点
parentElm.insertBefore(createElement(newStartVnode), oldStartVnode.elm);
} else {
// 说明不是全新的项,要移动
const elmToMove = oldCh[idxInOld];
patch(elmToMove, newStartVnode);
// 把这项设置为undefined,表示我已经处理完这项了
oldCh[idxInOld] = undefined;
// 移动,调用insertBefore也可以实现移动。
parentElm.insertBefore(elmToMove.elm, oldStartVnode.elm);
}
// newStartIdx++;
newStartVnode = newCh[++newStartIdx];
}
}
// 循环结束
if (newStartIdx <= newEndIdx) {
// 说明newVndoe还有剩余节点没有处理,所以要添加这些节点
for (let i = newStartIdx; i <= newEndIdx; i++) {
console.log('添加节点')
// insertBefore方法可以自动识别null,如果是null就会自动排到队尾,和appendChild一致
parentElm.insertBefore(createElement(newCh[i]), null);
}
} else if (oldStartIdx <= oldEndIdx) {
// 说明oldVnode还有剩余节点没有处理,所以要删除这些节点
for (let i = oldStartIdx; i <= oldEndIdx; i++) {
console.log('删除节点')
if (oldCh[i]) {
parentElm.removeChild(oldCh[i].elm);
}
}
}
}
// 判断是否是同一个节点
function checkSameVnode(a, b) {
return a.sel === b.sel && a.key === b.key;
}
1282

被折叠的 条评论
为什么被折叠?



