两个概念:
- Virtual Dom(虚拟dom)
- diff算法
虚拟dom
为什么需要虚拟dom
每次更新真实dom的开销非常大,如果修改数据之后直接渲染到真实dom上会引起页面的重绘和回流。 虚拟dom可以在结点改变之后和原结点比较,如果发现有变化就会修改真实dom,旧结点会被虚拟dom的新节点替换。
重绘:元素外观、颜色等不影响布局的属性改变;
回流:元素的布局、位置、尺寸改变
什么是虚拟dom
虚拟dom是类似与对象结构的存储的一种数据
dom:
<div>
<span>*</span>
</div>
对应的虚拟dom:
{
tag: 'div',
children: [
{ tag: 'span', text: '*' }
]
}
diff算法 (Vue2)
比较方式
同级比较,不会跨级比较,如果父级结点一样就会比较子节点
<div>
<span>*</span>
</div>
<div>
<p>1</p>
</div>
div和div比较,span和p比较
比较流程
patch
首先会判断新节点是否为空:空的话会卸载老节点, 判断老节点是否为空:空的话会创建新节点
工具函数:
- isUndef 判断是否未定义
- isDef 判断是否定义
- invokeDestroyHook 卸载结点
- createElm 创建结点
function patch(oldVnode, vnode) {
// 判断新的vnode是否为空
if (isUndef(vnode)) {
// 如果老的vnode不为空 卸载所有的老vnode
if (isDef(oldVnode)) {
invokeDestroyHook(oldVnode)
}
return
}
// 如果老节点不存在,直接创建新节点
if (isUndef(oldVnode)) {
createElm(vnode)
} else {
// 新老节点的type和key相同,进行patchVnode更新工作
if (sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode)
} else {
// 拿到 oldVnode 的父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
// 插在老节点的弟弟结点的前面 insertBefore
createElm(vnode, parentElm, nodeOps.nextSibling(oldElm))
// 如果旧节点还存在,就删掉旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 否则直接卸载 oldVnode
invokeDestroyHook(oldVnode)
}
}
}
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm
}
sameVnode
判断条件:
- key相同
- 标签名相同
- 注释节点标识相同(都是 或者 都不是)
- data值相同(都有值 或者 都没有值) key
function sameVnode (a, b) {
return (
a.key === b.key && // key值
a.tag === b.tag && // 标签名
a.isComment === b.isComment && // 是否为注释节点
// 是否都定义了data,data包含一些具体信息,例如onclick , style
isDef(a.data) === isDef(b.data) &&
sameInputType(a, b) // 当标签是<input>的时候,type必须相同
)
}
patchVnode
当两个结点都相同时会执行该方法,判断子节点是否相同
patchVnode (oldVnode, vnode) {
// 获取真实dom
const el = vnode.el = oldVnode.el
let i, oldCh = oldVnode.children, ch = vnode.children
// 如果两个节点都指向同一个对象直接return
if (oldVnode === vnode) return
// 比较两个节点的文本节点
if (oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text) {
// 不一样就把vnode的文本节点设置给el
api.setTextContent(el, vnode.text)
}else {
// 一样
if (oldCh && ch && oldCh !== ch) {
// 子节点不一样
updateChildren(el, oldCh, ch)
}else if (ch){
// oldv不存在子节点 则创建新节点
createEle(vnode) //create el's children dom
}else if (oldCh){
// v 不存在子节点 则移除oldv的子节点
api.removeChildren(el)
}
}
}
updateChildren
当两个节点的子节点不同时会调用该方法
updateChildren (parentElm, oldCh, newCh) {
let oldStartIdx = 0, newStartIdx = 0
let oldEndIdx = oldCh.length - 1
let oldStartVnode = oldCh[0]
let oldEndVnode = oldCh[oldEndIdx]
let newEndIdx = newCh.length - 1
let newStartVnode = newCh[0]
let newEndVnode = newCh[newEndIdx]
let oldKeyToIdx
let idxInOld
let elmToMove
let before
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
// 对于vnode.key的比较,会把oldVnode = null
// 判断节点是否为空,空的话索引值从两边向中间移动
if (oldStartVnode == null) {
oldStartVnode = oldCh[++oldStartIdx]
}else if (oldEndVnode == null) {
oldEndVnode = oldCh[--oldEndIdx]
}else if (newStartVnode == null) {
newStartVnode = newCh[++newStartIdx]
}else if (newEndVnode == null) {
newEndVnode = newCh[--newEndIdx]
}
// 不为空时判断两个节点是否相同,判断完成之后索引值向中间移动
else if (sameVnode(oldStartVnode, newStartVnode)) {
patchVnode(oldStartVnode, newStartVnode)
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
}else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode)
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldStartVnode, newEndVnode)) {
patchVnode(oldStartVnode, newEndVnode)
api.insertBefore(parentElm, oldStartVnode.el, api.nextSibling(oldEndVnode.el))
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
}else if (sameVnode(oldEndVnode, newStartVnode)) {
patchVnode(oldEndVnode, newStartVnode)
api.insertBefore(parentElm, oldEndVnode.el, oldStartVnode.el)
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
}
// 使用key时的比较
else {
if (oldKeyToIdx === undefined) {
// oldvnodeCh含有key值的生成key字典
oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx) // 有key生成index表
}
// vnch中节点key 去字典表中查找 { keyName: oldStartIdx, keyName1: oldStartIdx + 1, ... keyNameEnd: oldEndIdx }
idxInOld = oldKeyToIdx[newStartVnode.key]
if (!idxInOld) {
// 查不到则插入,索引++
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
newStartVnode = newCh[++newStartIdx]
}
else {
elmToMove = oldCh[idxInOld]
// 判断节点是否相同
if (elmToMove.sel !== newStartVnode.sel) {
api.insertBefore(parentElm, createEle(newStartVnode).el, oldStartVnode.el)
}else {
// 比较子节点
patchVnode(elmToMove, newStartVnode)
oldCh[idxInOld] = null
api.insertBefore(parentElm, elmToMove.el, oldStartVnode.el)
}
newStartVnode = newCh[++newStartIdx]
}
}
}
if (oldStartIdx > oldEndIdx) {
// 如果oldv 索引超出范围,则newv中剩下的节点添加到真实dom中,索引为原来的索引值
before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].el
addVnodes(parentElm, before, newCh, newStartIdx, newEndIdx)
}else if (newStartIdx > newEndIdx) {
// 移除oldv中剩余的子节点
removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
}
}
图解
无key的情况下:
有key的情况下:
在key的index表中查找对应的ov的key
- 没找到: 则直接新增v节点
- 找到:将 v 和 对应索引值的 ov节点比较
- 不同:直接新增v子节点
- 相同:清空ov中的该子节点,比较两子节点中的子节点,并且将ov子节点移动到当前未比较老节点首部
vue3 和 react的diff算法略有不同