持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第2天
| 虚拟DOM(VDOM) | |
|---|---|
| 定义 | 是一个 「js对象」, 是真实DOM的抽象, 比DOM轻量, 参考的是snabbdom |
| 好处 | 1.避免重复操作DOM, 避免重绘和回流,这样性能比较高; 2. 跨平台 |
vue2源码
/*
* oldVnode: 旧虚拟节点
* vnode: 新虚拟节点
* hydrating: 是否和真实dom混合
* removeOnly: 标识
*
*/
// path()入口
function patch (oldVnode, vnode, hydrating, removeOnly) {
// 情况1: vnode不存在, oldVnode存在, 移除oldVnode
if (isUndef(vnode)) {
if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
return
}
let isInitialPatch = false
const insertedVnodeQueue = []
// 情况2: oldVnode不存在,创建新节点
if (isUndef(oldVnode)) {
isInitialPatch = true
createElm(vnode, insertedVnodeQueue)
} else {
// 情况3: vnode存在,oldVnode存在
// 情况3-1: 判断oldVnode是否是虚拟odm
const isRealElement = isDef(oldVnode.nodeType)
// 情况3-2: 判断oldVnode 和 vnode 是不是同一节点? patchVode() : 挂载真实dom
if (!isRealElement && sameVnode(oldVnode, vnode)) {
patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
} else {
// 真实dom
if (isRealElement) {
...
}
...
}
}
}
// patchVnode()
function patchVnode (
oldVnode,
vnode,
insertedVnodeQueue,
ownerArray,
index,
removeOnly
) {
// 情况一: vnode === oldVnode 直接返回
if (oldVnode === vnode) {
return
}
...
const oldCh = oldVnode.children
const ch = vnode.children
...
if (isUndef(vnode.text)) {
if (isDef(oldCh) && isDef(ch)) {
// 情况二: oldCh !== ch
if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
} else if (isDef(ch)) {
// 情况三: oldVnode children不存在 添加节点
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(ch)
}
if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
} else if (isDef(oldCh)) {
// 情况四: vnode children不存在 删除oldCh节点
removeVnodes(oldCh, 0, oldCh.length - 1)
} else if (isDef(oldVnode.text)) {
nodeOps.setTextContent(elm, '')
}
} else if (oldVnode.text !== vnode.text) {
// 情况5: 都是文本,值不相等 - 修改
nodeOps.setTextContent(elm, vnode.text)
}
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
}
// updateChildren()
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
let oldStartIdx = 0 // old开始位置
let newStartIdx = 0 // new开始位置
let oldStartVnode = oldCh[0] // old 开始节点
let newStartVnode = newCh[0] // new 开始节点
let oldEndIdx = oldCh.length - 1 // old结束位置
let newEndIdx = newCh.length - 1 // new 结束位置
let oldEndVnode = oldCh[oldEndIdx] // old结束节点
let newEndVnode = newCh[newEndIdx] // new结束节点
let oldKeyToIdx, idxInOld, vnodeToMove, refElm
const canMove = !removeOnly
if (process.env.NODE_ENV !== 'production') {
checkDuplicateKeys(newCh)
}
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
patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// patch完成,移动位置
oldStartVnode = oldCh[++oldStartIdx]
newStartVnode = newCh[++newStartIdx]
} else if (sameVnode(oldEndVnode, newEndVnode)) {
// 情况二: 如果节点 尾 相等, 从右往左开始patchVnode
patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// patch完成,移动位置
oldEndVnode = oldCh[--oldEndIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldStartVnode, newEndVnode)) {
// 情况三: 老开始 == 新结束节点, 从右往左patchvnode
patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue, newCh, newEndIdx)
// 把oldStart节点放到oldEnd节点后面
canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
// patch 移动位置
oldStartVnode = oldCh[++oldStartIdx]
newEndVnode = newCh[--newEndIdx]
} else if (sameVnode(oldEndVnode, newStartVnode)) {
// 情况四: 老结束 == 新开始节点, 从右=左往右patchvnode
patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue, newCh, newStartIdx)
// 把oldEnd节点放到oldStart节点前面
canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
// patch 移动位置
oldEndVnode = oldCh[--oldEndIdx]
newStartVnode = newCh[++newStartIdx]
} else {
// 没有就创建元素
...
}
}
if (oldStartIdx > oldEndIdx) {
refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
} else if (newStartIdx > newEndIdx) {
removeVnodes(oldCh, oldStartIdx, oldEndIdx)
}
}
vue3源码
// 情况1: 没有key
const patchUnkeyedChildren = (
c1: VNode[], // 老节点
c2: VNodeArrayChildren, // 新节点
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
c1 = c1 || EMPTY_ARR
c2 = c2 || EMPTY_ARR
const oldLength = c1.length // 老节点长度
const newLength = c2.length // 新节点长度
const commonLength = Math.min(oldLength, newLength) // 小长度的作为公共长度
let i
// 重点是「 循环 」
for (i = 0; i < commonLength; i++) {
const nextChild = (c2[i] = optimized ?
cloneIfMounted(c2[i] as VNode) :
normalizeVNode(c2[i]))
patch(
c1[i],
nextChild,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// 老节点长度 > 新节点的长度 ? 删除多余节点 : 新增节点
if (oldLength > newLength) {
unmountChildren(
c1,
parentComponent,
parentSuspense,
true,
false,
commonLength
)
} else {
mountChildren(
c2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
commonLength
)
}
}
// 情况2: 有key--这是主要的diff算法
const patchKeyedChildren = (
c1: VNode[], // 老节点
c2: VNodeArrayChildren, // 新节点
container: RendererElement,
parentAnchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let i = 0 // 用于记录索引
const l2 = c2.length // 新节点长度
let e1 = c1.length - 1 // e1: 老节点结束位置
let e2 = l2 - 1 // e2: 新节点结束位置
// 从开始位置开始比较-循环
while (i <= e1 && i <= e2) {
const n1 = c1[i] // 老节点 第i个元素
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 节点相同时
if (isSameVNodeType(n1, n2)) {
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 节点不相同就 break
break
}
i++
}
// 第二步: 从结束位置开始比较-循环
while (i <= e1 && i <= e2) {
const n1 = c1[e1]
const n2 = (c2[e2] = optimized
? cloneIfMounted(c2[e2] as VNode)
: normalizeVNode(c2[e2]))
if (isSameVNodeType(n1, n2)) {
// 节点相同时
patch(
n1,
n2,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 节点不同时break
break
}
e1--
e2--
}
// 如果旧节点多 - 删除老节点, 如果新节点多 - 增加节点
if (i > e1) {
if (i <= e2) {
const nextPos = e2 + 1
const anchor = nextPos < l2 ? (c2[nextPos] as VNode).el : parentAnchor
while (i <= e2) {
patch(
null,
(c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i])),
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
i++
}
}
}
else if (i > e2) {
while (i <= e1) {
unmount(c1[i], parentComponent, parentSuspense, true)
i++
}
}
// 特殊情况处理:首尾相同中间乱序
// 接着1-2 的逻辑往下走
else {
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 把没有匹配到的新节点通过map保存「 newIndexToOldIndexMap」
const keyToNewIndexMap: Map<string | number, number> = new Map()
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 遍历新的节点,为新节点设置key
if (nextChild.key != null) {
if (__DEV__ && keyToNewIndexMap.has(nextChild.key)) {
...
}
keyToNewIndexMap.set(nextChild.key, i)
}
}
let j
let patched = 0 // 记录-已经patch的新节点的数量
const toBePatched = e2 - s2 + 1 // 记录-没有经过 path 新的节点的数量
let moved = false
let maxNewIndexSoFar = 0
const newIndexToOldIndexMap = new Array(toBePatched)
// 遍历老节点
for (i = 0; i < toBePatched; i++) newIndexToOldIndexMap[i] = 0
for (i = s1; i <= e1; i++) {
const prevChild = c1[i]
// 统一卸载老节点
if (patched >= toBePatched) {
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 老节点的key存在 ,通过key找到对应的index
newIndex = keyToNewIndexMap.get(prevChild.key)
} else {
// 老节点的key不存在, 遍历剩下的所有新节点
for (j = s2; j <= e2; j++) {
if (
newIndexToOldIndexMap[j - s2] === 0 &&
isSameVNodeType(prevChild, c2[j] as VNode)
) {
newIndex = j
break
}
}
}
if (newIndex === undefined) {
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
patch(
prevChild,
c2[newIndex] as VNode,
container,
null,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
patched++
}
}
// 5.3 move and mount
const increasingNewIndexSequence = moved
? getSequence(newIndexToOldIndexMap)
: EMPTY_ARR
j = increasingNewIndexSequence.length - 1
for (i = toBePatched - 1; i >= 0; i--) {
const nextIndex = s2 + i
const nextChild = c2[nextIndex] as VNode
const anchor =
nextIndex + 1 < l2 ? (c2[nextIndex + 1] as VNode).el : parentAnchor
if (newIndexToOldIndexMap[i] === 0) {
patch(
null,
nextChild,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
move(nextChild, container, anchor, MoveType.REORDER)
} else {
j--
}
}
}
}
}
vue2 和vue3 diff区别总结
| Diff算法 | Vue2 | Vue3 |
|---|---|---|
| 定义 | 通过「同层的树节点」进行比较的「高效算法」 | |
| 特点 | 1. 仅「同层级进行」 2. 比较过程中, 由两边向中间比较 | |
| 原理 | 递归 + 双指针 | 最长递增子序列 |
| 源码分析 |
1. 判断是不是同一元素,不是,则「直接替换」 2. patch(oldVnode, Vnode)- diff算法入口函数同一元素,比较{「 属性和children 」 a. oldVnode是否是虚拟dom b. oldVnode和Vnode是否是同一节点,否-重建 3. children五种情况:- patchVnode(精细化比较) a. child 新 == 老, return b. child 新有,老有,都是text, 不相等,修改即可 c. child新无,老有,删除老的 d. child新有,老无,「在老的上面创建子节点」 e. child新有,老有(「 双指针」updateChildren) 4. 双指针比对 updateChildren() a. 条件1: 头头比较, b. 条件2: 尾尾比较, . 条件3: 头尾比较, d. 条件4: 尾头比较 e. 条件5,abcd条件不满足,分两种情况 i. 无key: newStartVnode生成新的节点插入到真实DOM中 ii. 有key:未处理的节点的key组成的hash表对比 |
没有key: 1. 获取新老节点长度最小的,循环进行patch工作 2. 新节点长度 > 旧节点长度, 挂载公共部分之后的所有新节点 3. 新节点长度 < 旧节点长度, 卸载公共部分之后的所有老节点
有key: |