diff比较
patch(diff过程)
为什么在 VNode 层面上做 Patch ,而不是在VDOM层面上做 Patch
第一,要保证跨平台,cross platform
第二,不需要那么多的属性attribute,只针对当前所希望获取到的那部分属性去消费就好了
因此这里通过 虚拟对象(虚拟VDOM) 来进行判断,判断到底该用什么,怎么用它
<div>
<p>
<span>
123
</span>
</p>
</div>
- 同层比较
- 同一层级的同一元素比较,类型不一样,直接当前节点&所有子节点 销毁
- 类型一样,借助key
初始化时候,mount
然后进行 patch(diff)
vue2 diff 双端比较
数组的头和尾去比较
patch函数
function patch(oldVnode, vnode, hydrating, removeOnly) {
// 判断新的vnode是否为空
if (isUndef(vnode)) {
// 如果老的vnode不为空 卸载所有的老vnode
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
// 用来存储 insert钩子函数,在插入节点之前调用
const insertedVnodeQueue = []
// 如果老节点不存在,直接创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 是不是元素节点
const isRealElement = isDef(oldVnode.nodeType)
// 当老节点不是真实的DOM节点,并且新老节点的type和key相同,进行patchVnode更新工作
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 如果不是同一元素节点的话
// 当老节点是真实DOM节点的时候
if (isRealElement) {
// 如果是元素节点 并且在SSR环境的时候 修改SSR_ATTR属性
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
}
}
// 如果不是服务端渲染的,或者混合失败,就创建一个空的注释节点替换 oldVnode
oldVnode = emptyNodeAt(oldVnode)
}
// 拿到 oldVnode 的父节点
const oldElm = oldVnode.elm
const parentElm = nodeOps.parentNode(oldElm)
// 根据新的 vnode 创建一个 DOM 节点,挂载到父节点上
createElm(
vnode,
insertedVnodeQueue,
oldElm._leaveCb ? null : parentElm,
nodeOps.nextSibling(oldElm)
)
// 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新父节点
// 递归 更新父占位符元素
// 就是执行一遍 父节点的 destory 和 create 、insert 的 钩子函数
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
for (let i = 1; i < insert.fns.length; i++) {
insert.fns[i]()
}
}
} else {
registerRef(ancestor)
}
// 更新父节点
ancestor = ancestor.parent
}
}
// 如果旧节点还存在,就删掉旧节点
if (isDef(parentElm)) {
removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
// 否则直接卸载 oldVnode
invokeDestroyHook(oldVnode)
}
}
}
// 执行 虚拟 dom 的 insert 钩子函数
invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
// 返回最新 vnode 的 elm ,也就是真实的 dom节点
return vnode.elm
}
核心是这句:
// 当老节点不是真实的DOM节点,并且新老节点的type和key相同,进行patchVnode更新工作
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
}
patchVnode函数
前面所作的事情:将oldNode节点上的事件全都挂载到新的上处理
从if (isDef(data) && isPatchable(vnode)) 这句开始就比较重要了
function patchVnode(
oldVnode, // 老的虚拟 DOM 节点
vnode, // 新节点
insertedVnodeQueue, // 插入节点队列
ownerArray, // 节点数组
index, // 当前节点的下标
removeOnly
) {
// 新老节点对比地址一样,直接跳过
if (oldVnode === vnode) {
return
}
if (isDef(vnode.elm) && isDef(ownerArray)) {
// clone reused vnode
vnode = ownerArray[index] = cloneVNode(vnode)
}
const elm = vnode.elm = oldVnode.elm
// 如果当前节点是注释或 v-if 的,或者是异步函数,就跳过检查异步组件
if (isTrue(oldVnode.isAsyncPlaceholder)) {
if (isDef(vnode.asyncFactory.resolved)) {
hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
} else {
vnode.isAsyncPlaceholder = true
}
return
}
// 当前节点是静态节点的时候,key 也一样,或者有 v-once 的时候,就直接赋值返回
if (isTrue(vnode.isStatic) &&
isTrue(oldVnode.isStatic) &&
vnode.key === oldVnode.key &&
(isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
) {
vnode.componentInstance = oldVnode.componentInstance
return
}
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
const oldCh = oldVnode.children
const ch = vnode.children
if (isDef(data) && isPatchable(vnode)) {
// 遍历调用 update 更新 oldVnode 所有属性,比如 class,style,attrs,domProps,events...
// 这里的 update 钩子函数是 vnode 本身的钩子函数
for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
// 这里的 update 钩子函数是我们传过来的函数
if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
}
// 如果新节点不是文本节点,也就是说有子节点
if (isUndef(vnode.text)) {
// 如果新老节点都有子节点
if (isDef(oldCh) && isDef(ch)) {
// 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果新节点有子节点的话,就是说老节点没有子节点
// 如果老节点是文本节点,就是说没有子节点,就清空
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
// 添加新节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果新节点没有子节点,老节点有子节点,就删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
// 如果老节点是文本节点,就清空
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 如果老节点的文本和新节点的文本不同,就更新文本
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
这里最核心的内容:是这句:
// 如果新老节点的子节点不一样,就执行 updateChildren 函数,对比子节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
updateChildren
这个过程才是真正的双端比较,双端比较的过程就是所谓的指针的判断
也是当前的子节点进行双端比较
实现思路
dom上举例:
旧的 DOM(prevChildren):
<div id="app">
<ul>
<li key="1">Item A</li>
<li key="2">Item B</li>
<li key="3">Item C</li>
<li key="4">Item D</li>
</ul>
</div>
新的 DOM(nextChildren):
<div id="app">
<ul>
<li key="2">Item B</li>
<li key="3">Item C</li>
<li key="4">Item D</li>
<li key="5">Item E</li>
</ul>
</div>
类比下面的a,b,c,d
第一步:
function vue2Diff(prevChildren, nextChildren, parent) {
let oldStartIndex = 0,
oldEndIndex = prevChildren.length - 1,
newStartIndex = 0,
newEndIndex = nextChildren.length - 1;
let oldStartNode = prevChildren[oldStartIndex],
oldEndNode = prevChildren[oldEndIndex],
newStartNode = nextChildren[newStartIndex],
newEndNode = nextChildren[newEndIndex];
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
patch(oldStartNode, newStartNode, parent)
oldStartIndex++
newStartIndex++
oldStartNode = prevChildren[oldStartIndex]
newStartNode = nextChildren[newStartIndex]
} else if (oldEndNode.key === newEndNode.key) {
patch(oldEndNode, newEndNode, parent)
oldEndIndex--
newndIndex--
oldEndNode = prevChildren[oldEndIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldStartNode.key === newEndNode.key) {
patch(oldvStartNode, newEndNode, parent)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
老新:头头,尾尾,头尾,尾头的情况
头尾一样的情况:
老的头放到新的尾
尾头一样的情况:
老的尾放到新的头上
第二步:移动
尾头一样的情况:
老的尾放到新的头上
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
// ...
} else if (oldEndNode.key === newStartNode.key) {
patch(oldEndNode, newStartNode, parent)
// 移动到旧列表头节点之前
parent.insertBefore(oldEndNode.el, oldStartNode.el)
oldEndIndex--
newStartIndex++
oldEndNode = prevChildren[oldEndIndex]
newStartNode = nextChildren[newStartIndex]
}
}
}
第三步:移动
头尾一样的情况:
老的头放到新的尾
function vue2Diff(prevChildren, nextChildren, parent) {
// ...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
// ...
} else if (oldEndNode.key === newEndNode.key) {
// ...
} else if (oldStartNode.key === newEndNode.key) {
patch(oldStartNode, newEndNode, parent)
parent.insertBefore(oldStartNode.el, oldEndNode.el.nextSibling)
oldStartIndex++
newEndIndex--
oldStartNode = prevChildren[oldStartIndex]
newEndNode = nextChildren[newEndIndex]
} else if (oldEndNode.key === newStartNode.key) {
//...
}
}
}
第三步,如果以上情况都没匹配到,就只能是纯找了
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
}
}
}
找到的情况:
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
if (oldIndex > -1) {
//找到了情况
let oldNode = prevChildren[oldIndex];
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
没有找到,就创建一个新的:
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// 在旧列表中找到 和新列表头节点key 相同的节点
let newtKey = newStartNode.key,
oldIndex = prevChildren.findIndex(child => child.key === newKey);
if (oldIndex > -1) {
let oldNode = prevChildren[oldIndex];
patch(oldNode, newStartNode, parent)
parent.insertBefore(oldNode.el, oldStartNode.el)
prevChildren[oldIndex] = undefined
} else {
mount(newStartNode, parent, oldStartNode.el)
}
newStartNode = nextChildren[++newStartIndex]
}
}
}
最后当旧列表遍历到undefind时就跳过当前节点。
function vue2Diff(prevChildren, nextChildren, parent) {
//...
while (oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
if (oldStartNode === undefind) {
oldStartNode = prevChildren[++oldStartIndex]
} else if (oldEndNode === undefind) {
oldEndNode = prevChildren[--oldEndIndex]
} else if (oldStartNode.key === newStartNode.key) {
//...
} else if (oldEndNode.key === newEndNode.key) {
//...
} else if (oldStartNode.key === newEndNode.key) {
//...
} else if (oldEndNode.key === newStartNode.key) {
//...
} else {
// ...
}
}
}
Vue3最长递增子序列
- 非全量diff
- 静态标记 vnode static
在vue3创建vnode节点过程中,会加入static这个值,当dom不变,当内容节点不更新的时候,我们这个值的属性也不会更新
举例:
Hello World
hei World
相同的那部分去掉
=>
头往后走
llo World
i World
=>
尾往前走
llo
i
function vue3Diff(prevChildren, nextChildren, parent) {
let j = 0,
prevEnd = prevChildren.length - 1,
nextEnd = nextChildren.length - 1,
prevNode = prevChildren[j],
nextNode = nextChildren[j];
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
j++
prevNode = prevChildren[j]
nextNode = nextChildren[j]
}
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
while (prevNode.key === nextNode.key) {
patch(prevNode, nextNode, parent)
prevEnd--
nextEnd--
prevNode = prevChildren[prevEnd]
nextNode = prevChildren[nextEnd]
}
}
A B C D
D B A C
- 最长递增子序列是:A C,那么保证A和C位置不变
- D在老的最后一位,新的第一位,将D从最后一位挪到第一位
- 在新的时候,B在A前,就insertBefore将B注入到A之前
updateChildren这里不一样,其他点都一样。