同层比较会发生的情况:
- 节点类型发生变化
- 节点类型不变,属性发生变化
- 增/删/改 节点
- 文本变化
Vue2
patch
function patch(oldVnode, newVnode) { // 传入新、旧节点
// 比较是否为一个类型的节点
if (sameVnode(oldVnode, newVnode)) {
// 是:继续进行深层比较
patchVnode(oldVnode, newVnode)
} else {
// 否
const oldEl = oldVnode.el // 旧虚拟节点的真实DOM节点
const parentEle = api.parentNode(oldEl) // 获取父节点
createEle(newVnode) // 创建新虚拟节点对应的真实DOM节点
if (parentEle !== null) {
api.insertBefore(parentEle, newVnode.el, api.nextSibling(oldEl)) // 将新元素添加进父元素
api.removeChild(parentEle, oldVnode.el) // 移除以前的旧元素节点
// 设置null,释放内存
oldVnode = null
}
}
return newVnode
}
sameVnode
function sameVnode(oldVnode, newVnode) {
return (
oldVnode.key === newVnode.key && // key 值是否一样
oldVnode.tagName === newVnode.tagName && // 标签名是否一样
oldVnode.isComment === newVnode.isComment && // 是否都为注释节点
isDef(oldVnode.data) === isDef(newVnode.data) && // 是否都定义了 data
sameInputType(oldVnode, newVnode) // 当标签为 input 时,type 必须是否相同
)
}
patchVNode
function patchVnode(oldVnode, newVnode) {
const el = newVnode.el = oldVnode.el // 获取真实DOM对象
// 获取新旧虚拟节点的子节点数组
const oldCh = oldVnode.children, newCh = newVnode.children
// 如果新旧虚拟节点是同一个对象,则终止
if (oldVnode === newVnode) return
// 如果新旧虚拟节点是文本节点,且文本不一样
if (oldVnode.text !== null && newVnode.text !== null && oldVnode.text !== newVnode.text) {
// 则直接将真实DOM中文本更新为新虚拟节点的文本
api.setTextContent(el, newVnode.text)
} else {
if (oldCh && newCh && oldCh !== newCh) {
// 新旧虚拟节点都有子节点,且子节点不一样
// 对比子节点,并更新
/* diff核心!!*/
updateChildren(el, oldCh, newCh)
} else if (newCh) {
// 新虚拟节点有子节点,旧虚拟节点没有
// 创建新虚拟节点的子节点,并更新到真实DOM上去
createEle(newVnode)
} else if (oldCh) {
// 旧虚拟节点有子节点,新虚拟节点没有
// 直接删除真实DOM里对应的子节点
api.removeChild(el)
}
}
}
- 拿到真实的 dom 节点 el(即 oldVnode)
- 判断当前 newVnode 和 oldVnode 是否指向同一个对象,如果是则直接 return
- 如果是文本节点,且文本有变化,则直接调用api 将文本替换;若文本没有变化,则继续对比新旧节点的子节点 children
- 如果 oldVnode 有子节点而 newVnode 没有,则删除 el 的子节点
- 如果 oldVnode 没有子节点而 newVnode 有,则将 newVnode 的子节点真实化之后添加到 el
- 如果两者都有子节点,则执行 updateChildren 函数比较子节点,这一步很重要—-diff 的核心
updateChildren
此方法就是diff算法的核心部分,当发现新旧虚拟节点的的子节点都存在时候,我们就需要通过一些方法来判断哪些节点是需要移动的,哪些节点是可以直接复用的,来提高我们整个diff的效率;
- Vue 的 diff 算法是同级比较,不考虑跨级比较的情况。内部采用深度递归的方式 + 双指针的方式进行比较。
- 1.先比较是否是相同节点 key tag
- 2.相同节点比较属性,并复用老节点
- 3.比较儿子节点,考虑老节点和新节点儿子的情况
- 4.优化比较:头头、尾尾、头尾、尾头
- 5.比对查找进行复用
双端对比
所谓 双端比较 就是新列表和旧列表两个列表的头与尾互相对比,在对比的过程中指针会逐渐向内靠拢,直到某一个列表的节点全部遍历过,对比停止。
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex){....}
- 使用以上四步进行对比,去寻找 key相同的可复用的节点,当在某一步中找到了则停止后面的寻找。
- 当对比时找到了可复用的节点,我们还是先 patch 给元素打补丁,然后将指针进行前/后移一位指针。根据对比节点的不同,我们移动的指针和方向也不同
什么情况下DOM节点需要移动 / DOM节点如何移动
// a b c d 旧
// d b a c 新
#循环1
发现 oldEndNode = newStartNode 移动 d 到开头 => d a b c
oldEndIndex-- newStartIndex++
#循环2
发现尾节点相同, oldEndIndex-- newEndIndex--
#循环3
发现 oldStartNode = newEndNode 移动a到b后面 => d b a c
oldStartNode++ newEndIndex--
#循环4
b = b 四个指针也相同,退出循环
// a b c d 旧
// b d a c 新
#循环1
没找到,拿新列表的第一个节点去旧列表中找与其key相同的节点。
找到对应的VNode,我们只需要将找到的节点的DOM元素,移动到开头,旧列表中的节点改为undefined
如果没找到直接创建一个新的节点放到最前面
newStartIndex++
Vue3
最长递增子序列
- 头和头比
- 尾和尾比
- 基于最长递增子序列进行移动/添加/删除
看个例子,比如
老的 children:[ a, b, c, d, e, f, g ]
新的 children:[ a, b, f, c, d, e, h, g ]
- 先进行头和头比,发现不同就结束循环,得到 [ a, b ]
- 再进行尾和尾比,发现不同就结束循环,得到 [ g ]
- 再保存没有比较过的节点 [ f, c, d, e, h ],并通过 newIndexToOldIndexMap 拿到在数组里对应的下标,生成数组 [ 5, 2, 3, 4, -1 ],-1 是老数组里没有的就说明是新增
- 然后再拿取出数组里的最长递增子序列,也就是 [ 2, 3, 4 ] 对应的节点 [ c, d, e ]
- 然后只需要把其他剩余的节点,基于 [ c, d, e ] 的位置进行移动/新增/删除就可以了
我们从后向前进行遍历source每一项。此时会出现三种情况:
- 当前的值为-1,这说明该节点是全新的节点,又由于我们是从后向前遍历,我们直接创建好 DOM 节点插入到队尾就可以了。
- 当前的索引为最长递增子序列中的值,也就是i === seq[j],这说说明该节点不需要移动
- 当前的索引不是最长递增子序列中的值,那么说明该 DOM 节点需要移动,这里也很好理解,我们也是直接将 DOM 节点插入到队尾就可以了,因为队尾是排好序的。
React
仅右移动
附录
为什么同层比较
如果不同层比较的话,全部的对比完一整个 dom 结构,时间复杂度是 O(n^3) ; 时间成本太大了;所以改用同层比较这样的方法去牺牲了精度而提高了时间效率。
事件复杂度变成O(n)
为什么使用 key
- 匹配了key,则之移动元素 — 性能较好
- 未匹配key,则删除重建 — 性能较差
dom
<div id="test">
<span style="color:red">test1</span> test2
</div>
var dom = document.getElementById('test');
dom.innerHTML // <span style="color:red">test1</span> test2
dom.innerText // test1 test2
dom.outerHTML // <div id="test"><span style="color:red">test1</span> test2</div>
dom..textContent // = innerText