diff算法
虚拟DOM是什么
虚拟DOM是表示真实DOM的JS对象
真实DOM
<!-- 真实DOM -->
<div class="container">
<p class="item">虚拟DOM</p>
<strong class="item">JS对象</strong>
</div>
虚拟DOM:真实DOM的JS对象
/*
Vnode 为上边真实DOM的JS对象。
里面包含的字段有标签名,以及标签属性及子标签的名称、属性和文本节点。
*/
let Vnode = {
tagName: 'div',
props: {
'class': 'container'
},
children: [
{
tagName: 'p',
props: {
'class': 'item'
},
text: '虚拟DOM'
},
{
tagName: 'strong',
props: {
'class': 'item'
},
text: 'JS对象'
}
]
}
什么是diff算法
diff算法的目的就是找出(两个虚拟DOM)差异,使最小化的更新视图。本质上就是比较两个JS对象的差异
整体流程
当数据改变的时候,就会触发内部的setter方法,进一步触发dep.notify方法,然后通知到各数据使用方,执行patch方法。patch方法接收到两个参数新旧虚拟节点,首先在内部需要判断一下,是不是同类标签,如果不是同类标签,就没有比对的必要直接替换就可以了;如果是同类标签的话,那就需要进一步执行patchVnode方法,在这个方法内部,也是首先需要判断一下新旧虚拟节点是否相等,如果相等的话那就没有比对的必要了,直接return,如果不相等,那就需要分情况来比对,比对的原则就是以新虚拟节点的结果为准:
- 第一种情况是,旧虚拟节点和新虚拟节点都有文本节点,直接用新的文本替换旧文本
- 第二种情况是,旧虚拟节点没有子节点,新虚拟节点有子节点,直接添加新的子节点
- 第三种情况是,旧虚拟节点有子节点,新虚拟节点没有子节点,直接删除旧的子节点
- 第四种情况是,旧新虚拟节点都有子节点,这种情况,我们就需要比对他们的子节点,通过
updateChildren方法,专门来比对他们的子节点。
updateChildren方法
- 内部规定了只在同级比对,减少比对次数,最大化的提高比对性能。
- 首尾指针法
首尾指针法
不管是新旧虚拟节点,都有首尾两个元素,对应的是start和end。旧虚拟节点的start和新虚拟节点start,做比对,如果没有比对成功,旧虚拟节点的start和新虚拟节点的end,做比对,如果依旧没有比对成功,旧虚拟节点的end和新虚拟节点的start,做比对,如果依旧没有成功,旧虚拟节点的end和新虚拟节点的end做比对。
- 依次比较,当比较成功后退出当前比较
- 渲染结果以newVnode为准
- 每次比较成功的start和end点向中间靠拢
- 当新旧节点中有一个start点跑到end点右侧时终止比对
- 如果都匹配不到,则旧虚拟DOM key值去比对新虚拟DOM key值,如果key值相同则复用,并移动到新虚拟DOM的位置。
整体流程
patch(vnode, oldVnode)
- 新虚拟节点不存在,旧虚拟节点存在,直接删除旧虚拟节点(调用旧虚拟节点的
destroy钩子函数) - 新虚拟节点存在,旧虚拟节点不存在,通过
createElm创建DOM元素,并将其挂载到文档。 - 如果新旧虚拟节点都存在,通过
sameVnode判断是不是相同节点,相同需要进一步执行patchVnode,不相同直接替换
patchVnode(oldVnode,vnode)
- 判断新旧虚拟节点是否相等,无需比较直接返回
- 新虚拟节点为文本节点
- 新旧虚拟文本节点是否相等,不相等更新文本
- 新虚拟节点不是文本节点
- 新虚拟节点存在子节点,旧虚拟节点不存在子节点,新增所有的子节点
- 新虚拟节点不存在子节点,旧虚拟节点存在子节点,删除所有的子节点
- 新旧虚拟节点都有子节点,执行
updateChildren(elm,oldCh,ch)方法, 比较孩子节点
updateChildren(parentElm,oldCh,newCh)
-
首先这个方法传入三个比较重要的参数,既 parentElm 父级真实节点,便于直接操作; oldCh 为oldVnode的孩子节点,newCh 为Vnode 的孩子节点。
-
oldCh和newCh都是数组。这个方法的作用就是对这两个数组一一比较,找到相同的节点,执行patchVnode 再次进行比较更新,剩下的新增或者删除。
-
内部规定了只在同级比对,减少比对次数,最大化的提高比对性能。
-
首尾指针法
- 不管是新旧虚拟节点,都有首尾两个元素,对应的是
start和end。旧虚拟节点的start和新虚拟节点start,做比对,如果没有比对成功,旧虚拟节点的start和新虚拟节点的end,做比对,如果依旧没有比对成功,旧虚拟节点的end和新虚拟节点的start,做比对,如果依旧没有成功,旧虚拟节点的end和新虚拟节点的end做比对。 sameVnode比对成功,进一步进行patchVnode,每次比较成功的start和end点向中间靠拢- 当新旧节点中有一个start点跑到end点右侧时终止比对
- 如果四种方法(新旧虚拟节点的交叉比对)都匹配不到相同节点的话,剩下的只能使用暴力解法去实现,也就是针对于 newStartVnode 这个节点,我们去遍历 oldCh 中剩余的节点,一一匹配
- 生成一个 oldCh 得key-> index 的映射表,用变量 oldKeyToIdx 去存储,如果新虚拟孩子节点存在key值,直接用,oldKeyToIdx[newStartVnode.key] 拿到对应旧孩子节点的下标 index;如果没有key值,通过遍历 oldCh 中剩余得节点,一一进行匹配获取对应下标 index。
- 如果oldCh匹配不到index,则创建新节点
- 如果key值相同,但节点不同,创建新节点
- key值相同,节点也相同,进行patchVnode
- 这时候,我们 oldCh 和 newCh 两个数组一一比较差不多了,
- 不管是新旧虚拟节点,都有首尾两个元素,对应的是
-
如果这个时候,oldCh 的两个指针已经重叠并越过,而 newCh 的两个指针还未重叠;说明 newCh 有多余的 vnode ,我们只需要新增他们就可以了
-
或者相反情况下(newCh 的两个指针已经重叠并越过,而 oldCh 的两个指针还未重叠)说明 oldCh 有多余的 vnode ,我们只需要删除他们即可。
源码
-
isUndef(vnode): 这个函数用于检查vnode是否未定义或者为null。如果vnode不存在,即isUndef(vnode)返回true,那么表示没有新的虚拟节点需要渲染。 -
如果
vnode不存在,那么代码继续执行下一步:isDef(oldVnode): 这个函数用于检查oldVnode是否已经被定义,即旧的虚拟节点是否存在。如果旧的虚拟节点oldVnode存在,即isDef(oldVnode)返回true,那么说明之前有一个虚拟节点被渲染到了 DOM 上。invokeDestroyHook(oldVnode): 这个函数用于调用旧虚拟节点上的销毁钩子函数。在 Vue.js 中,组件有一系列的生命周期钩子函数,其中包括destroyed钩子,用于在组件被销毁时执行一些清理操作。invokeDestroyHook的作用就是执行旧虚拟节点上的销毁钩子函数。
return function patch(oldVnode, vnode, hydrating, removeOnly) {
// 1、新虚拟节点不存在,旧虚拟节点存在,调用旧虚拟节点的destroy钩子函数
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue: any[] = []
// 2、新虚拟节点存在,旧虚拟节点不存在,创建新的DOM元素,并进行跟踪。创建完成之后并将其挂载到文档中。
if (isUndef(oldVnode)) {
// empty mount (likely as component), create new root element
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 3、如果新旧虚拟节点都存在,判断是不是相同节点,相同需要进一步执行patchNode,不相同直接替换
const isRealElement = isDef(oldVnode.nodeType)
if (!isRealElement && sameVnode(oldVnode, vnode)) {
// patch existing root node
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
if (isRealElement) {
// mounting to a real element
// check if this is server-rendered content and if we can perform
// a successful hydration.
if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
oldVnode.removeAttribute(SSR_ATTR)
hydrating = true
}
if (isTrue(hydrating)) {
if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
invokeInsertHook(vnode, insertedVnodeQueue, true)
return oldVnode
} else if (__DEV__) {
warn(
'The client-side rendered virtual DOM tree is not matching ' +
'server-rendered content. This is likely caused by incorrect ' +
'HTML markup, for example nesting block-level elements inside ' +
'<p>, or missing <tbody>. Bailing hydration and performing ' +
'full client-side render.'
)
}
}
// either not server-rendered, or hydration failed.
// create an empty node and replace it
oldVnode = emptyNodeAt(oldVnode)
}
// replacing existing element
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// create new node
createElm(
vnode,
insertedVnodeQueue,
// extremely rare edge case: do not insert if old element is in a
// leaving transition. Only happens when combining transition +
// keep-alive + HOCs. (#4590)
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// update parent placeholder node element, recursively
if (isDef(vnode.parent)) {
let ancestor = vnode.parent
const patchable = isPatchable(vnode)
while (ancestor) {
for (let i = 0; i < cbs.destroy.length; ++i) {
cbs.destroy[i](ancestor)
}
ancestor.elm = vnode.elm
if (patchable) {
for (let i = 0; i < cbs.create.length; ++i) {
cbs.create[i](emptyNode, ancestor)
}
// #6513
// invoke insert hooks that may have been merged by create hooks.
// e.g. for directives that uses the "inserted" hook.
const insert = ancestor.data.hook.insert
if (insert.merged) {
// start at index 1 to avoid re-invoking component mounted hook
// clone insert hooks to avoid being mutated during iteration.
// e.g. for customed directives under transition group.
const cloned = insert.fns.slice(1)
for (let i = 0; i < cloned.length; i++) {
cloned[i]()
}
}
} else {
registerRef(ancestor)
}
ancestor = ancestor.parent
}
}
// destroy old node
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
invokeDestroyHook(oldVnode)
}
}
}
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
return vnode.elm
}