此篇是对渲染更新的优化,当模板发生变化之后,我们可以利用 diff 算法对比新旧 dom,看是否能节点复用
思考:
当我们初始渲染完成的 1 秒后,数据发生了变化, Vue 怎么处理显示最新的值呢?
- 把上次渲染的真实 dom 删除 然后重新渲染一个新的 dom 节点来应用最新的 a 的值
- 把老的 dom 进行复用 改变一下内部文本节点的 textContent 的值
很明显这里后者的性能开销最小,diff 算法就是采用的后者。
一、patch 核心渲染方法改写
- oldVnode 是真实 dom ,就代表是初次渲染;
- oldVnode 是虚拟 dom,就是更新过程,采用 diff 算法;
- 如果新旧标签不一致 用新的替换旧的 oldVnode.el 代表的是真实 dom 节点--同级比较
新旧节点替换的 3 种情况:
- 新旧标签不一致,则直接替换;
- 新旧文本节点不一致,则将旧文本节点内容替换成新文本节点的 textContent 的值;
- 如果标签一致,且不是文本节点,则进行
节点复用,直接把旧的虚拟dom对应的真实dom赋值给新的虚拟dom的el属性;
src/vdom/patch.js
我们直接看 else 分支 代表的是渲染更新过程 可以分为以下几步
1.diff 只进行同级比较
2.根据新老 vnode 子节点不同情况分别处理
二、 updateProperties 更新属性
- 如果新的节点没有节点属性, 需要把老的节点属性移除;
- 对style样式做特殊处理, 如果新的没有 需要把老的style值置为空;
- 遍历新的属性, 进行增加操作;
src/vdom/patch.js
对比新老 vnode 进行属性更新
三、 updateChildren 更新子节点-diff 核心方法
- 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用;
- diff算法核心 采用双指针的方式 对比新老vnode的儿子节点;
- 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置;
循环老节点,while函数
- 只有当新老儿子的双指标的起始位置不大于结束位置的时候 才能循环 一方停止了就需要结束循环;
- 因为暴力对比过程把移动的vnode置为 undefined 如果不存在vnode节点 直接跳过;
- 头和头对比 依次向后追加;
- 尾和尾对比 依次向前追加;
- 老的头和新的尾相同 把老的头部移动到尾部;
- 老的尾和新的头相同 把老的尾部移动到头部;
- 上述四种情况都不满足 那么需要暴力对比;
- 根据老的子节点的key和index的映射表 从新的开始子节点进行查找 如果可以找到就进行移动操作 如果找不到则直接进行插入;
// 只有当新老儿子的双指标的起始位置不大于结束位置的时候 才能循环 一方停止了就需要结束循环
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
// 因为暴力对比过程把移动的vnode置为 undefined 如果不存在vnode节点 直接跳过
if (!oldStartVnode) {
oldStartVnode = oldCh[++oldStartIndex];
} else if (!oldEndVnode) {
oldEndVnode = oldCh[--oldEndIndex];
} else if (isSameVnode(oldStartVnode, newStartVnode)) {
// 头和头对比,依次向后追加
patch(oldStartVnode, newStartVnode); // 递归比较他们的儿子及其子节点
oldStartVnode = oldCh[++oldStartIndex];
newStartVnode = newCh[++newStartIndex];
} else if (isSameVnode(oldEndVnode, newEndVnode)) {
// 尾和尾对比,依次向前追加
patch(oldEndVnode, newEndVnode);
oldEndVnode = oldCh[--oldEndIndex];
newEndVnode = newCh[--newEndIndex];
} else if (isSameVnode(oldStartVnode, newEndVnode)) {
// 老的头和新的尾相同,则把老的头移动到尾部
patch(oldStartVnode, newEndVnode);
parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling);
oldStartVnode = oldCh[++oldStartIndex];
newEndVnode = newCh[--newEndIndex];
} else if (isSameVnode(oldEndVnode, newStartVnode)) {
// 老的尾和新的头相同,则把老的尾移动到头部
patch(oldEndVnode, newStartVnode);
parent.insertBefore(oldEndVnode.el, oldStartVnode.el);
oldEndVnode = oldCh[--oldEndIndex];
newStartVnode = newCh[++newStartIndex];
} else {
// 以上情况都不符,则暴力对比
// 根据老的子节点的key和index的映射表 从新的开始子节点进行查找 如果可以找到就进行移动操作 如果找不到则直接进行插入
let moveIndex = map[newStartVnode.key];
if (!moveIndex) { // 老节点找不到 直接插入
parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
} else { // 找得到就拿老的节点
let moveVnode = oldCh[moveIndex];
oldCh[moveIndex] = undefined; //这个是占位操作 避免数组塌陷 防止老节点移动走了之后破坏了初始的映射表位置
parent.insertBefore(moveVnode.el, oldStartVnode.el); //插在旧节点的最前面
patch(moveVnode, newStartVnode);
}
}
}
- 如果老节点循环完毕了 但是新节点还有 证明 新节点需要被添加到头部或者尾部;
- 如果新节点循环完毕 老节点还有 证明老的节点需要直接被删除;
对 updateChildren 函数的总结:
1.使用双指针移动来进行新老节点的对比
2.用 isSameVnode 来判断新老子节点的头头 尾尾 头尾 尾头 是否是同一节点 如果满足就进行相应的移动指针(头头 尾尾)或者移动 dom 节点(头尾 尾头)操作
3.如果全都不相等 进行暴力对比 如果找到了利用 key 和 index 的映射表来移动老的子节点到前面去 如果找不到就直接插入
4.对老的子节点进行递归 patch 处理
5.最后老的子节点有多的就删掉 新的子节点有多的就添加到相应的位置
总结一句就是,能复用就复用,能移动就移动,实在不行才选择新增或删除。
四、改造原型渲染更新方法 _update
-
初次渲染 vm._vnode肯定不存在 要通过虚拟节点 渲染出真实的dom 赋值给$el属性;
-
更新时把上次的vnode和这次更新的vnode穿进去 进行diff算法;
src/lifecycle.js
改造_update 方法 在 Vue 实例的_vnode 保留上次的 vnode 节点 以供 patch 进行新老虚拟 dom 的对比
系列文章参考:
作者:前端鲨鱼哥
链接:juejin.cn/post/695343…
来源:稀土掘金