上一篇主要介绍了 Vue
异步更新原理是对视图更新的性能优化。此篇同样是对渲染更新的优化,当模板发生变化之后,可以利用 diff
算法,对比新老虚拟 dom
看是否能进行节点复用。
<script>
let vm = new Vue({
el: "#app",
data() {
return {
a: 123,
};
},
template: `<div id="a">hello {{a}}</div>`,
});
setTimeout(() => {
vm.a = 1;
}, 1000);
</script>
思考:如果我们当初始渲染完成 1
秒后改变了一下模板里面 a
的值 Vue
会怎么处理来显示最新的值呢?
-
把上次渲染的真实
dom
删除,然后重新渲染一个新的dom
节点来应用最新的a
的值 -
把老的
dom
进行复用,改变一下内部文本节点的textContent
的值
这两种方案 很明显
后者
的性能开销更小
一起来看看 Vue
是怎么使用 diff
算法来进行渲染更新的吧
1. patch 核心渲染方法改写
// src/vdom/patch.js
export function patch(oldVnode, vnode) {
const isRealElement = oldVnode.nodeType;
if (isRealElement) {
// oldVnode是真实dom元素 就代表初次渲染
} else {
// oldVnode是虚拟dom 就是更新过程 使用diff算法
if (oldVnode.tag !== vnode.tag) {
// 如果新旧标签不一致 用新的替换旧的 oldVnode.el代表的是真实dom节点--同级比较
oldVnode.el.parentNode.replaceChild(createElm(vnode), oldVnode.el);
}
// 如果旧节点是一个文本节点
if (!oldVnode.tag) {
if (oldVnode.text !== vnode.text) {
oldVnode.el.textContent = vnode.text;
}
}
// 不符合上面两种 代表标签一致 并且不是文本节点
// 为了节点复用 所以直接把旧的虚拟dom对应的真实dom赋值给新的虚拟dom的el属性
const el = (vnode.el = oldVnode.el);
updateProperties(vnode, oldVnode.data); // 更新属性
const oldCh = oldVnode.children || []; // 老的儿子
const newCh = vnode.children || []; // 新的儿子
if (oldCh.length > 0 && newCh.length > 0) {
// 新老都存在子节点
updateChildren(el, oldCh, newCh);
} else if (oldCh.length) {
// 老的有儿子新的没有
el.innerHTML = "";
} else if (newCh.length) {
// 新的有儿子
for (let i = 0; i < newCh.length; i++) {
const child = newCh[i];
el.appendChild(createElm(child));
}
}
}
}
我们直接看 else
分支,代表的是渲染更新过程,可以分为以下几步:
diff
只进行同级比较
- 根据新老
vnode
子节点不同情况分别处理
2. updateProperties 更新属性
// src/vdom/patch.js
// 解析vnode的data属性 映射到真实dom上
function updateProperties(vnode, oldProps = {}) {
const newProps = vnode.data || {}; //新的vnode的属性
const el = vnode.el; // 真实节点
// 如果新的节点没有 需要把老的节点属性移除
for (const k in oldProps) {
if (!newProps[k]) {
el.removeAttribute(k);
}
}
// 对style样式做特殊处理 如果新的没有 需要把老的style值置为空
const newStyle = newProps.style || {};
const oldStyle = oldProps.style || {};
for (const key in oldStyle) {
if (!newStyle[key]) {
el.style[key] = "";
}
}
// 遍历新的属性 进行增加操作
for (const key in newProps) {
if (key === "style") {
for (const styleName in newProps.style) {
el.style[styleName] = newProps.style[styleName];
}
} else if (key === "class") {
el.className = newProps.class;
} else {
// 给这个元素添加属性 值就是对应的值
el.setAttribute(key, newProps[key]);
}
}
}
对比新老 vnode
进行属性更新
3. updateChildren 更新子节点-diff 核心方法
// src/vdom/patch.js
// 判断两个vnode的标签和key是否相同 如果相同 就可以认为是同一节点就地复用
function isSameVnode(oldVnode, newVnode) {
return oldVnode.tag === newVnode.tag && oldVnode.key === newVnode.key;
}
// diff算法核心 采用双指针的方式 对比新老vnode的儿子节点
function updateChildren(parent, oldCh, newCh) {
let oldStartIndex = 0; //老儿子的起始下标
let oldStartVnode = oldCh[0]; //老儿子的第一个节点
let oldEndIndex = oldCh.length - 1; //老儿子的结束下标
let oldEndVnode = oldCh[oldEndIndex]; //老儿子的起结束节点
let newStartIndex = 0; //同上 新儿子的
let newStartVnode = newCh[0];
let newEndIndex = newCh.length - 1;
let newEndVnode = newCh[newEndIndex];
// 根据key来创建老的儿子的index映射表 类似 {'a':0,'b':1} 代表key为'a'的节点在第一个位置 key为'b'的节点在第二个位置
function makeIndexByKey(children) {
let map = {};
children.forEach((item, index) => {
map[item.key] = index;
});
return map;
}
// 生成的映射表
let map = makeIndexByKey(oldCh);
// 只有当新老儿子的双指标的起始位置不大于结束位置的时候 才能循环 一方停止了就需要结束循环
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); //insertBefore可以移动或者插入真实dom
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);
}
}
}
// 如果老节点循环完毕了 但是新节点还有 证明 新节点需要被添加到头部或者尾部
if (newStartIndex <= newEndIndex) {
for (let i = newStartIndex; i <= newEndIndex; i++) {
// 这是一个优化写法 insertBefore的第一个参数是null等同于appendChild作用
const ele =
newCh[newEndIndex + 1] == null ? null : newCh[newEndIndex + 1].el;
parent.insertBefore(createElm(newCh[i]), ele);
}
}
// 如果新节点循环完毕 老节点还有 证明老的节点需要直接被删除
if (oldStartIndex <= oldEndIndex) {
for (let i = oldStartIndex; i <= oldEndIndex; i++) {
let child = oldCh[i];
if (child != undefined) {
parent.removeChild(child.el);
}
}
}
}
这段代码特别长,但是理解起来其实可以简单的分为以下几点:
- 使用
双指针
移动来进行新老节点的对比
-
用
isSameVnode
来判断新老子节点的头头、尾尾、头尾、尾头是否是同一节点,如果满足就进行相应的移动指针(头头 尾尾)或者移动dom
节点(头尾 尾头)操作 -
如果全都不相等,进行暴力对比。如果找到了,利用
key
和index
的映射表来移动老的子节点到前面去,如果找不到就直接插入
-
对老的子节点进行递归
patch
处理 -
最后老的子节点有多的就删掉,新的子节点有多的就添加到相应的位置
4. 改造原型渲染更新方法_update
// src/lifecycle.js
export function lifecycleMixin(Vue) {
// 把_update挂载在Vue的原型
Vue.prototype._update = function (vnode) {
const vm = this;
const prevVnode = vm._vnode; // 保留上一次的vnode
vm._vnode = vnode;
if (!prevVnode) {
// patch是渲染vnode为真实dom核心
vm.$el = patch(vm.$el, vnode); // 初次渲染 vm._vnode肯定不存在 要通过虚拟节点 渲染出真实的dom 赋值给$el属性
} else {
vm.$el = patch(prevVnode, vnode); // 更新时把上次的vnode和这次更新的vnode穿进去 进行diff算法
}
};
}
改造 _update
方法。在 Vue
实例的 _vnode
保留上次的 vnode
节点,以供 patch
进行新老虚拟 dom
的对比。
5. diff 算法的思维导图
声明:本文仅供个人学习使用,来源于互联网,本文有改动,本文遵循[BY-NC-SA]协议, 如有侵犯您的权益,请联系本站,本站将在第一时间删除。谢谢你