diff 算法简介
1.diff算法-同层比较
- 在日常开发中,很少发生将父亲节点和儿子节点进行交换的场景
- 性能瓶颈
综合这两点,diff算法仅对同层节点进行对比
2.比较方式
dom
是一个树型结构,diff算法是将新、老两个虚拟节点树进行比对。
从根节点开,同层对比,diff
比对是“深度优先遍历”的递归比对
- 两个节点不是同一个节点,直接删除老的,换上新的
- 两个节点是同一个节点,复用老节点,将差异属性更新
- 节点比较完成后,比较两个节点的儿子节点
Vue2与Vue3在节点更新时的性能对比:
1.递归比对是vue2的性能瓶颈,当组件树庞大时会产生性能问题;
2.在vue3中,会收集动态节点,并对他们的变化进行标记,根据标记进行更新,而无需使用diff递归比对一遍
3.Vue3是线性比对,而Vue2是两棵树的比对,效率上会比vue2高出很多;
3.如何确定两个节点是同一个节点
标签相同并且key值相同
一般节点没有key值,标签相同既可以复用,当标签相同不希望复用时可以使用key属性对节点进行标记,key值不相同,即使标签名相同的两个元素,也不会进行复用。
// src/vdom/index.js
/**
* 判断两个虚拟节点是否是同一个虚拟节点
* @param {*} newVnode 新虚拟节点
* @param {*} oldVnode 老虚拟节点
* @returns
*/
export function isSameVnode(newVnode, oldVnode){
// 判断逻辑:tag 标签名 和 key 完全相同
return (newVnode.tag === oldVnode.tag)&&(newVnode.key === oldVnode.key);
}
实现diff算法
整体流程:
- 不是同一节点直接替换
- 是同一节点
- 特殊情况:文本,文本的
tag
是undefined
,判断如果节点的tag
为undefined
,则直接用新的文本替换老的文本。 - 正常节点
- 属性替换
调用patchProps方法,需要对style进行单独处理 - 对比儿子节点,判断五种情况
- 属性替换
- 特殊情况:文本,文本的
1.不是同一节点直接替换
根据isSameVnode
方法判断是否为同一节点
function patchVNode(oldVNode, vnode) {
if (!isSameVnode(oldVNode, vnode)) {
let el = createElm(vnode)
oldVNode.el.parentNode.replaceChild(createElm(vnode), oldVNode.el)
return el
} else {
return
}
}
2.是同一节点
2.1文本情况
由于vNode
和oldVNode
的tag相同,只需要判断其中一个的tag
为undefined
即可
// 将老节点赋值给新节点
let el = vnode.el = oldVNode.el;
// 相同可能是文本的情况 文本的tag是undefined
if (!oldVNode.tag) {
if (oldVNode.text !== vnode.text) {
el.textContent = vnode.text;//用新的文本覆盖旧的文本
}
}
2.2属性替换
重构patchProps
方法
对于style
需要进行特殊处理,如果新旧节点都有style
,style
的属性值为字符串类型,不能直接进行替换,需要对样式属性进行收集,再进行比较和更新;
export function patchProps(el, oldProps = {}, props = {}) {
let oldStyles = oldProps.style || {};
let newStyles = props.style || {};
for (let key in oldStyles) { // 老的样式中有 新的没有则删除
if (!newStyles[key]) {
el.style[key] = ''
}
}
for (let key in oldProps) { // 老的属性中有
if (!props[key]) { // 新的没有删除属性
el.removeAttribute(key);
}
}
for (let key in props) { // 用新的覆盖老的
if (key === 'style') { // style{color:'red'}
for (let styleName in props.style) {
el.style[styleName] = props.style[styleName];
}
} else {
el.setAttribute(key, props[key]);
}
}
}
2.3对比儿子节点
- 新的有儿子,老的没有,直接赋值
- 新的没有儿子,老的有,直接删除
- 新的老的都有,进行处理
let oldChildren = oldVNode.children || [];
let newChildren = vnode.children || [];
if (oldChildren.length > 0 && newChildren.length > 0) {
// 完整的diff算法 需要比较两个人的儿子
updateChildren(el, oldChildren, newChildren);
} else if (newChildren.length > 0) { // 没有老的,有新的
mountChildren(el, newChildren);
} else if (oldChildren.length > 0) { // 新的没有 老的有 要删除
el.innerHTML = ''; // 可以循环删除
}
return el
添加节点
function mountChildren(el, newChildren) {
for (let i = 0; i < newChildren.length; i++) {
let child = newChildren[i];
el.appendChild(createElm(child))
}
}
在真正执行diff
比对前,针对于【情况 1】“老的有儿子,新的没有儿子”和【情况 2】“老的没有儿子,新的有儿子”这两种特殊情况,优先进行了特殊处理;
当以上两种情况均不满足,即【情况 3】新老节点都有儿子时,就必须进行diff
比对了;
所以,updateChildren
方法才是diff算法的核心;