
在vue是如何组合起来的文章中我们知道虚拟DOM就是以对象形式描述的DOM,也了解了啥时候调用patch算法进行虚拟Dom比对。
但之前都是用简化版的去实现,具体怎么比对,Vnode长啥样,我们这就来学学。
再补充一下,变化侦测只通知到组件级别。为了减小性能浪费,这也是产生Vnode的一个原因。
Vnode长啥样
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
__v_isVNode: true
[ReactiveFlags.SKIP]: true
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | symbol | null
ref: VNodeNormalizedRef | null
scopeId: string | null
slotScopeIds: string[] | null
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number
suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
appContext: AppContext | null
memo?: any[]
isCompatRoot?: true
ce?: (instance: ComponentInternalInstance) => void
}
根据Vnode的定义,可见其就是一个普通的对象。
Vode的类型
export type VNodeTypes =
| string
| VNode
| Component
| typeof Text
| typeof Static
| typeof Comment
| typeof Fragment
| typeof TeleportImpl
| typeof SuspenseImpl
不同类型的Vnode之间其实只是有效属性不同,创建Vnode实例时,无效的属性会有默认赋值
可以创建不同类型的Vnode
// 创建文本节点
export function createTextVNode(text: string = ' ', flag: number = 0): VNode {
return createVNode(Text, null, text, flag)
}
// 创建静态节点
export function createStaticVNode(
content: string,
numberOfNodes: number
): VNode {
const vnode = createVNode(Static, null, content)
vnode.staticCount = numberOfNodes
return vnode
}
// 创建注释节点
export function createCommentVNode(
text: string = '',
asBlock: boolean = false
): VNode {
return asBlock
? (openBlock(), createBlock(Comment, null, text))
: createVNode(Comment, null, text)
}
patch
终于到虚拟Dom的核心部分了
patch作用
对比两个Vnode之间的差异只是patch的一部分,这是手段,不是目的。patch是在现有的DOM上进行修改达到渲染视图的目的。所以要做三件事
- 创建新增的节点
- 删除废弃的节点
- 修改更新的节点
看看源码
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
新增节点
这通常发生在首次渲染中。因为在首次渲染中,oldVnode不存在任何节点。
还有一种情况是当oldVnode和vnode完全不是同一个节点时,vnode是一个全新的节点,而oldNode是废弃的节点。
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
删除节点
在vnode中不存在的节点都是废弃的,需要删除
更新节点
静态节点
静态节点不会因状态的变化而发生变化,比对时可跳过更新操作
const patchStaticNode = (
n1: VNode,
n2: VNode,
container: RendererElement,
isSVG: boolean
) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
const anchor = hostNextSibling(n1.anchor!)
// remove existing
removeStaticNode(n1)
// insert new
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
isSVG
)
} else {
n2.el = n1.el
n2.anchor = n1.anchor
}
}
新虚拟节点有文本属性
当两个虚拟节点都不是静态节点时且有不同属性时,要以新的为准,如果新虚拟节点有文本属性可直接替换
新虚拟节点无文本属性
如果新虚拟节点无文本属性,两个都有children还要进行更细致的比对,若新节点无children,说明是空节点,将旧节点也变为空节点。
更新策略(diff算法)
创建子节点
当在oldChildren中没有找到与本次循环所指向的新子节点相同的节点,需要执行创建节点操作。
将创建的节点插入到oldChildren所以未处理的节点前面
移动子节点
当比对发现是同一节点但位置不同时,需要移动到所有未处理的节点前面
删除子节点
遍历后删除所有标记未处理的节点
优化策略
通常情况下,并不是所有子节点的位置都会发生移动,所以可以采用一下4种查找方式进行优化,注意查找的范围都是在未处理数组的前面
新前与旧前
新后与旧后
新后与旧前
新前与旧后
如果以上4种方式都不行再采用循环的方式
如何发现哪些节点是未处理的
建立四个变量:oldStartIdx、oldEndInx、newStartInx、newEndInx
while (oldStartInx <= oldEndInx && newStartInx <= newEndInx) {
// 更新操作
}
if (newStartInx <= newEndInx) {
// 范围内都是新增节点
}
if (oldStartInx <= oldEndInx) {
// 范围内都是废弃节点
}
我觉得这哥们讲的挺好的