Diff算法核⼼原理——源码
在 Vue ⾥⾯Diff 算法就是 patch
⼀、patch(源码地址:src/core/vdom/patch.js -700⾏)
其实 patch 就是⼀个函数,先介绍⼀下源码⾥的核⼼流程,再来看⼀下 patch 的源码,源码⾥每⼀⾏也有注释1、可以接收四个参数,主要还是前两个
oldVnode:⽼的虚拟 DOM 节点
vnode:新的虚拟 DOM 节点
hydrating:是不是要和真实 DOM 混合,服务端渲染的话会⽤到,这⾥不过多说明
removeOnly:transition-group 会⽤到,这⾥不过多说明
2、主要流程是这样的:
vnode 不存在,oldVnode 存在,就删掉 oldVnode
vnode 存在,oldVnode 不存在,就创建 vnode
两个都存在的话,通过 sameVnode 函数(后⾯有详解)对⽐是不是同⼀节点
如果是同⼀节点的话,通过 patchVnode 进⾏后续对⽐节点⽂本变化或⼦节点变化
如果不是同⼀节点,就把 vnode 挂载到 oldVnode 的⽗元素下
如果组件的根节点被替换,就遍历更新⽗节点,然后删掉旧的节点
如果是服务端渲染就⽤ hydrating 把 oldVnode 和真实 DOM 混合
下⾯看完整的 patch 函数源码,注释⾥有说明:
1// 两个判断函数
2function isUndef (v: any): boolean %checks {
3return v === undefined || v === null
4 }
5function isDef (v: any): boolean %checks {
6return v !== undefined && v !== null
7 }
8return function patch (oldVnode, vnode, hydrating, removeOnly) {
9// 如果新的 vnode 不存在,但是 oldVnode 存在
10if (isUndef(vnode)) {
11// 如果 oldVnode 存在,调⽤ oldVnode 的组件卸载钩⼦ destroy
12if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
13return
14 }
15
16 let isInitialPatch = false
17 const insertedVnodeQueue = []
18
19// 如果 oldVnode 不存在的话,新的 vnode 是肯定存在的,⽐如⾸次渲染的时候
20if (isUndef(oldVnode)) {
21 isInitialPatch = true
22// 就创建新的 vnode
23 createElm(vnode, insertedVnodeQueue)
24 } else {
25// 剩下的都是新的 vnode 和 oldVnode 都存在的话
26
27// 是不是元素节点
28 const isRealElement = deType)
29// 是元素节点 && 通过 sameVnode 对⽐是不是同⼀个节点 (函数后⾯有详解)
30if (!isRealElement && sameVnode(oldVnode, vnode)) {
31// 如果是就⽤ patchVnode 进⾏后续对⽐ (函数后⾯有详解)
32 patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
33 } else {
34// 如果不是同⼀元素节点的话
35if (isRealElement) {
36// const SSR_ATTR = 'data-server-rendered'
37// 如果是元素节点并且有 'data-server-rendered' 这个属性
38if (deType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
39// 就是服务端渲染的,删掉这个属性
40 veAttribute(SSR_ATTR)
41 hydrating = true
42 }
43// 这个判断⾥是服务端渲染的处理逻辑,就是混合
44if (isTrue(hydrating)) {
45if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
46 invokeInsertHook(vnode, insertedVnodeQueue, true)
47return oldVnode
48 } else if (v.NODE_ENV !== 'production') {
49 warn('这是⼀段很长的警告信息')
50 }
51 }
52// function emptyNodeAt (elm) {
53// return new VNode(nodeOps.tagName(elm).toLowerCase(), {}, [], undefined, elm) 54// }
55// 如果不是服务端渲染的,或者混合失败,就创建⼀个空的注释节点替换 oldVnode
56 oldVnode = emptyNodeAt(oldVnode)
57 }
58
59// 拿到 oldVnode 的⽗节点
60 const oldElm = oldVnode.elm
61 const parentElm = nodeOps.parentNode(oldElm)
62
63// 根据新的 vnode 创建⼀个 DOM 节点,挂载到⽗节点上
64 createElm(
65 vnode,
66 insertedVnodeQueue,
67 oldElm._leaveCb ? null : parentElm,
68 Sibling(oldElm)
69 )
70
71// 如果新的 vnode 的根节点存在,就是说根节点被修改了,就需要遍历更新⽗节点
72if (isDef(vnode.parent)) {
73 let ancestor = vnode.parent
74 const patchable = isPatchable(vnode)
75// 递归更新⽗节点下的元素
76while (ancestor) {
77// 卸载⽼根节点下的全部组件
78for (let i = 0; i < cbs.destroy.length; ++i) {
79 cbs.destroy[i](ancestor)
80 }
81// 替换现有元素
82 ancestor.elm = vnode.elm
83if (patchable) {
84for (let i = 0; i < ate.length; ++i) {
85 ate[i](emptyNode, ancestor)
86 }
87 const insert = ancestor.data.hook.insert
88if (d) {
89for (let i = 1; i < insert.fns.length; i++) {
90 insert.fns[i]()
91 }
92 }
93 } else {
94 registerRef(ancestor)
95 }
96// 更新⽗节点
97 ancestor = ancestor.parent
98 }
99 }
100// 如果旧节点还存在,就删掉旧节点
101if (isDef(parentElm)) {
102 removeVnodes([oldVnode], 0, 0)
103 } else if (isDef(oldVnode.tag)) {
104// 否则直接卸载 oldVnode
105 invokeDestroyHook(oldVnode)
106 }
107 }
108 }
109// 返回更新后的节点
110 invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
111return vnode.elm
112 }
3、sameVnode
源码地址:src/core/vdom/patch.js -35⾏
这个是⽤来判断是不是同⼀节点的函数,源码:
function sameVnode (a, b) {
return (
a.key ===
b.key && // key 是不是⼀样
a.asyncFactory ===
b.asyncFactory && ( // 是不是异步组件
(
a.tag ===
b.tag && // 标签是不是⼀样
a.isComment ===
b.isComment && // 是不是注释节点
isDef(a.data) === isDef(b.data) && // 内容数据是不是⼀样
sameInputType(a, b) // 判断 input 的 type 是不是⼀样
) || (
isTrue(a.isAsyncPlaceholder) && // 判断区分异步组件的占位符否存在
isUndef()
)
)
)
}
4、patchVnode
源码地址:src/core/vdom/patch.js -501⾏
这个是在新的 vnode 和 oldVnode 是同⼀节点的情况下,才会执⾏的函数,主要是对⽐节点⽂本变化或⼦节点变化
4.1、主要流程是这样的:
如果 oldVnode 和 vnode 的引⽤地址是⼀样的,就表⽰节点没有变化,直接返回
如果 oldVnode 的 isAsyncPlaceholder 存在,就跳过异步组件的检查,直接返回
如果 oldVnode 和 vnode 都是静态节点,并且有⼀样的 key,并且 vnode 是克隆节点或者 v-once 指令控制的节点时,把oldVnode.elm 和 oldVnode.child 都复制到 vnode 上,然后返回
如果 vnode 不是⽂本节点也不是注释的情况下
如果 vnode 和 oldVnode 都有⼦节点,⽽且⼦节点不⼀样的话,就调⽤ updateChildren 更新⼦节点
如果只有 vnode 有⼦节点,就调⽤ addVnodes 创建⼦节点
如果只有 oldVnode 有⼦节点,就调⽤ removeVnodes 删除该⼦节点
如果 vnode ⽂本为 undefined,就删掉 vnode.elm ⽂本
如果 vnode 是⽂本节点但是和 oldVnode ⽂本内容不⼀样,就更新⽂本
function patchVnode (
oldVnode, // ⽼的虚拟 DOM 节点
vnode, // 新的虚拟 DOM 节点
insertedVnodeQueue, // 插⼊节点的队列
ownerArray, // 节点数组
index, // 当前节点的下标
removeOnly // 只有在
) {
// 新⽼节点引⽤地址是⼀样的,直接返回
// ⽐如 props 没有改变的时候,⼦组件就不做渲染,直接复⽤
if (oldVnode === vnode) return
// 新的 vnode 真实的 DOM 元素
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(solved)) {
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))
) {
vnodeponentInstance = oldVnodeponentInstance
return
}
// hook 相关的不⽤管
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,
// 这⾥的 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 ()) {
// 如果新⽼节点都有⼦节点
if (isDef(oldCh) && isDef(ch)) {
// 如果新⽼节点的⼦节点不⼀样,就执⾏ updateChildren 函数,对⽐⼦节点
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 如果新节点有⼦节点的话,就是说⽼节点没有⼦节点
// 如果⽼节点⽂本节点,就是说没有⼦节点,就清空
if ()) nodeOps.setTextContent(elm, '')
// 添加⼦节点
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 如果新节点没有⼦节点,⽼节点有⼦节点,就删除
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if ()) {
/
/ 如果⽼节点是⽂本节点,就清空
nodeOps.setTextContent(elm, '')
}
} else if ( !== ) {
// 新⽼节点都是⽂本节点,且⽂本不⼀样,就更新⽂本
nodeOps.setTextContent(elm, )
}
js获取子元素if (isDef(data)) {
// 执⾏ postpatch 钩⼦
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
5、updateChildren
源码地址:src/core/vdom/patch.js -404⾏
这个是新的 vnode 和 oldVnode 都有⼦节点,且⼦节点不⼀样的时候进⾏对⽐⼦节点的函数,这⾥很关键,很关键!
⽐如现在有两个⼦节点列表对⽐,对⽐主要流程如下:
循环遍历两个列表,循环停⽌条件是:其中⼀个列表的开始指针 startIdx 和结束指针 endIdx 重合
循环内容是:{
新的头和⽼的头对⽐
新的尾和⽼的尾对⽐
新的头和⽼的尾对⽐
新的尾和⽼的头对⽐。这四种对⽐如图
以上四种只要有⼀种判断相等,就调⽤ patchVnode 对⽐节点⽂本变化或⼦节点变化,然后移动对⽐的下标,继续下⼀轮循环对⽐
如果以上四种情况都没有命中,就不断拿新的开始节点的 key 去⽼的 children ⾥
如果没到,就创建⼀个新的节点
如果到了,再对⽐标签是不是同⼀个节点
如果是同⼀个节点,就调⽤ patchVnode 进⾏后续对⽐,然后把这个节点插⼊到⽼的开始前⾯,并且移动新的开始下标,继续下⼀轮循环对⽐
如果不是相同节点,就创建⼀个新的节点
}
如果⽼的 vnode 先遍历完,就添加新的 vnode 没有遍历的节点
如果新的 vnode 先遍历完,就删除⽼的 vnode 没有遍历的节点
为什么会有头对尾,尾对头的操作?
因为可以快速检测出 reverse 操作,加快 Diff 效率
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // ⽼ vnode 遍历的下标
let newStartIdx = 0 // 新 vnode 遍历的下标
let oldEndIdx = oldCh.length - 1 // ⽼ vnode 列表长度
let oldStartVnode = oldCh[0] // ⽼ vnode 列表第⼀个⼦元素
let oldEndVnode = oldCh[oldEndIdx] // ⽼ vnode 列表最后⼀个⼦元素
let newEndIdx = newCh.length - 1 // 新 vnode 列表长度
let newStartVnode = newCh[0] // 新 vnode 列表第⼀个⼦元素
let newEndVnode = newCh[newEndIdx] // 新 vnode 列表最后⼀个⼦元素
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
// 循环,规则是开始指针向右移动,结束指针向左移动移动
// 当开始和结束的指针重合的时候就结束循环
while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
if (isUndef(oldStartVnode)) {
oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
} else if (isUndef(oldEndVnode)) {
oldEndVnode = oldCh[--oldEndIdx]
// ⽼开始和新开始对⽐
} else if (sameVnode(oldStartVnode, newStartVnode)) {
// 是同⼀节点递归调⽤继续对⽐这两个节点的内容和⼦节点
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx) // 然后把指针后移⼀位,从前往后依次对⽐
// ⽐如第⼀次对⽐两个列表的[0],然后⽐[1]...,后⾯同理
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
// ⽼结束和新结束对⽐
} else if (sameVnode(oldEndVnode, newEndVnode)) {
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 然后把指针前移⼀位,从后往前⽐
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
// ⽼开始和新结束对⽐
} else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, Sibling(oldEndVnode.elm)) // ⽼的列表从前往后取值,新的列表从后往前取值,然后对⽐
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
// ⽼结束和新开始对⽐
} else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// ⽼的列表从后往前取值,新的列表从前往后取值,然后对⽐
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
// 以上四种情况都没有命中的情况
} else {
if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
// 拿到新开始的 key,在⽼的 children ⾥去有没有某个节点有这个 key
idxInOld = isDef(newStartVnode.key)
oldKeyToIdx[newStartVnode.key]
: findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
// 新的 children ⾥有,可是没有在⽼的 children ⾥到对应的元素
if (isUndef(idxInOld)) {
/// 就创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) } else {
// 在⽼的 children ⾥到了对应的元素
vnodeToMove = oldCh[idxInOld]
// 判断标签如果是⼀样的
if (sameVnode(vnodeToMove, newStartVnode)) {
// 就把两个相同的节点做⼀个更新
patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
oldCh[idxInOld] = undefined
canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
} else {
// 如果标签是不⼀样的,就创建新的元素
createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx) }
}
newStartVnode = newCh[++newStartIdx]
}
}
// oldStartIdx > oldEndIdx 说明⽼的 vnode 先遍历完
if (oldStartIdx > oldEndIdx) {
// 就添加从 newStartIdx 到 newEndIdx 之间的节点
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
// 否则就说明新的 vnode 先遍历完
} else if (newStartIdx > newEndIdx) {
// 就删除掉⽼的 vnode ⾥没有遍历的节点
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。
发表评论