在浏览器中,每次操作dom,都会引起一次重绘重排过程,如果短时间进行多次操作,对性能损耗很大,容易引起卡顿。
在vue中,使用虚拟dom(Virtual dom),来对真实dom的一种抽象化处理的树结构,模拟真实dom,提升性能。
而在更新dom节点时,通过对虚拟dom的对比diff(如果是更新操作)来进行对虚拟dom结构的增删改的一系列操作流程,就是patch过程。
在解读源码之前,需要知道一下vnode中的几个属性以及“最长递增子序列”算法,以便对patch流程阅读更加清晰。
最长递增子序列算法
在对比数组类型的子节点时,当新节点不是旧节点的简单push新增或pop删除等操作,有移动删除新增等操作,会使用此算法算出最小操作节点组合,用最少的执行步骤完成对比。
详情请到LeetCode查看,里面有大量解题思路,最常见的解法是动态规划
而vue里面用了维基百科里的贪心+二分查找解法去做,相比动态规划,时间复杂度能更小。
动态规划:O(n^2);
贪心+二分查找:O(nlogn);
leetcode 300. 最长递增子序列
PatchFlags.FRAGMENT:
什么情况下会为此值,STABLE_FRAGMENT、KEYED_FRAGMENT、UNKEYED_FRAGMENT必定有一个fragment包裹。
1、一个组件有多个根节点,会为其创建一个包裹:STABLE_FRAGMENT
2、v-if,有多个子节点: STABLE_FRAGMENT
3、v-for语句: KEYED_FRAGMENT、UNKEYED_FRAGMENT
// 源码v-for指令创建节点时fragmentFlag变量就是判断是否使用哪个。
const fragmentFlag = isStableFragment ? PatchFlags.STABLE_FRAGMENT : keyProp ? PatchFlags.KEYED_FRAGMENT : PatchFlags.UNKEYED_FRAGMENT;
// 其余其他属性忽略,有兴趣请自行查看
interface VNode {
// 节点的key属性,被当作节点的标志,用以优化
key: string | number | symbol | null,
// 一个枚举值,是一个标识,描述该组件的类型,值是位运算左移的结果
shapeFlag: number,
// 一个枚举值,也是一个标识,描述组件的特性,帮助实现vue3中patch对比的一个特性:靶向更新
// 值大于0,即代表所对应的element在patch阶段,可以进行优化diff
// 值小于0,即代表所对应的element在patch阶段,不需要进行diff
// 重点:patchFlag可以代表多个状态组合
patchFlag: number,
// 存储子组件的变量
// 纯文本 数组 slot对象
children: string | VNodeArrayChildren | RawSlots | null
}
export const enum PatchFlags {
// 动态的文本节点
TEXT = 1,
// 动态的class节点
CLASS = 1 << 1,
// 动态的style节点
STYLE = 1 << 2,
// 动态属性节点
PROPS = 1 << 3,
// 有动态key的节点,每当key改变,需要进行完整的diff
FULL_PROPS = 1 << 4,
// 绑定了监听事件
HYDRATE_EVENTS = 1 << 5,
// 不会变换子节点顺序的fragment
STABLE_FRAGMENT = 1 << 6,
// 有带key的fragment
KEYED_FRAGMENT = 1 << 7,
// 没有带key的fragment
UNKEYED_FRAGMENT = 1 << 8,
// 一个子节点只会进行非props比较
NEED_PATCH = 1 << 9,
// 动态slot 比如带v-for的slot插槽
DYNAMIC_SLOTS = 1 << 10,
// 以下类型不会被diff
// 开发阶段注释文本,不需要diff
DEV_ROOT_FRAGMENT = 1 << 11,
// 静态节点,不需要diff
HOISTED = -1,
// 用来表示一个节点不需要优化模式optimized,patch时进行全比对
BAIL = -2
}
export const enum ShapeFlags {
// 普通html节点
ELEMENT = 1,
// 函数组件
FUNCTIONAL_COMPONENT = 1 << 1,
// 普通有状态组件
STATEFUL_COMPONENT = 1 << 2,
// 子组件是纯文本
TEXT_CHILDREN = 1 << 3,
// 子组件是数组列表
ARRAY_CHILDREN = 1 << 4,
// 子组件有slot插槽
SLOTS_CHILDREN = 1 << 5,
// vue3 Teleport组件,具体请查看官方文档
TELEPORT = 1 << 6,
// vue3 Supspense组件,具体请查看官方文档
SUSPENSE = 1 << 7,
// 准备被KeepAlive的组件
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,
// 已经被KeepAlive的组件
COMPONENT_KEPT_ALIVE = 1 << 9,
// 函数组件或普通有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}
在vnode生成过程中,如果patchFlag>0(含有动态属性、动态class、动态style、动态文案等),则会将含有动态属性的children节点映射到此属性上,进行标记,那么在diff时,就不需要全量对children遍历对比子节点,只需对dynamicChildren进行对比操作,可以减少遍历次数,加快过程。
本文会讲出patch流程,但由于patch整体流程太过长,对于不同元素渲染会有特别处理(Fragment、自定义组件、TeleportImpl),特别处理部分不会涉及到,只总结出主要逻辑。
如有兴趣,可下载vue3源码打开packages > runtime-core > src > renderer.ts 阅读。
const patch: PatchFn = (
// 旧节点
n1,
// 新节点
n2,
// 节点容器,父节点
container,
// 要以哪个节点为标准插入进这个节点的前一个位置,为空则插入到最后
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
// optimized 是否优化vNode
// __DEV__ 开发环境
// isHmrUpdating开发热更新模式
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 等于就是没变化不用对比,跳过此处节点
if (n1 === n2) {
return
}
// 如果tag和key值都不一样,则删除此就节点
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(/* 忽略参数 */)
n1 = null
}
// 是否启用统一vnode处理
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, ref, shapeFlag } = n2
switch (type) {
// 文本类型
case Text:
processText(/* 忽略参数 */)
break
// 注释类型
case Comment:
processCommentNode(/* 忽略参数 */)
break
// 静态节点,不会变化
case Static:
// 没有直接渲染,有则不用渲染,因为不会变化
if (n1 == null) {
mountStaticNode(/* 忽略参数 */)
} else if (__DEV__) {
patchStaticNode(/* 忽略参数 */)
}
break
// Fragment 类型
case Fragment:
processFragment(/* 忽略参数 */)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 普通标签处理
processElement(/* 忽略参数 */)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 自定义组件的处理
processComponent(/* 忽略参数 */)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// Teleport组件(vue原生组件)的处理
; (type as typeof TeleportImpl).process(/* 忽略参数 */)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// Suspense组件(vue原生组件)处理
; (type as typeof SuspenseImpl).process(/* 忽略参数 */)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// 设置dom的ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
// 对比新旧的tag值以及key值
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
流程:
unmount函数主要功能是卸载相关节点并执行删除操作,如注册了事件,则进行派发。
代码里unmountChildren最终也只是将子节点使用remove方法删除,比如遇到虚拟节点(fragments),unmountChildren将传入此fragments的child进行remove;
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs
} = vnode;
// 删除解绑 ref
if (ref != null) {
setRef(ref, null, parentSuspense, vnode, true)
}
// 如果是准备keepAlive的组件,直接使之无效化
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
; (parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
// 是否为普通节点并绑定了指令
const shouldInvokeDirs = shapeFlag & ShapeFlags.ELEMENT && dirs
// 是否能调用钩子事件,非异步组件可调用
const shouldInvokeVnodeHook = !isAsyncWrapper(vnode)
let vnodeHook: VNodeHook | undefined | null;
// 如果有onVnodeBeforeUnmount事件,则执行调用
if (shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeBeforeUnmount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
if (shapeFlag & ShapeFlags.COMPONENT) {
// 对于自定义组件(函数组件与自定义组件都算)
unmountComponent(vnode.component!, parentSuspense, doRemove)
} else {
// suspense组件处理
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
vnode.suspense!.unmount(parentSuspense, doRemove)
return
}
// 如果为普通节点并绑定了指令,则执行beforeUnmount回调
if (shouldInvokeDirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'beforeUnmount')
}
// Teleport组件(vue原生组件)的处理
if (shapeFlag & ShapeFlags.TELEPORT) {
; (vnode.type as typeof TeleportImpl).remove(/* 忽略参数 */)
} else if (
dynamicChildren && (type !== Fragment || (patchFlag > 0 && patchFlag & PatchFlags.STABLE_FRAGMENT))
) {
// 如果有dynamic优化,那么只需卸载dynamicChildren里面的节点,其余静态节点可直接删除
// fast path for block nodes: only need to unmount dynamic children.
unmountChildren(/* 忽略参数 */)
} else if (
(type === Fragment && patchFlag & (PatchFlags.KEYED_FRAGMENT | PatchFlags.UNKEYED_FRAGMENT))
||
(!optimized && shapeFlag & ShapeFlags.ARRAY_CHILDREN)
) {
// 如果为Fragment,由于Fragment是一个虚拟节点(不会渲染到实际dom中),所以对Fragment子节点进行卸载删除
unmountChildren(/* 忽略参数 */)
}
// 执行删除操作
if (doRemove) {
remove(vnode)
}
}
// 如果注册了onVnodeUnmounted, 则派发onVnodeUnmounted和unmounted事件
if ((shouldInvokeVnodeHook && (vnodeHook = props && props.onVnodeUnmounted)) || shouldInvokeDirs) {
queuePostRenderEffect(() => {
vnodeHook &&
invokeVNodeHook(vnodeHook, parentComponent, vnode)
shouldInvokeDirs &&
invokeDirectiveHook(vnode, null, parentComponent, 'unmounted')
}, parentSuspense)
}
}
这里会主要关注新旧对比的代码。
注意:patchFlag的属性同时可存在多个
流程:
如果需要全量对比dom,那么过程又是如何,请继续往下看
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 创建渲染
mountElement(/* 忽略参数 */)
} else {
// 进行对比
patchElement(/* 忽略参数 */)
}
}
// 对比元素
const patchElement = (
n1: VNode,
n2: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
const el = (n2.el = n1.el!)
let { patchFlag, dynamicChildren, dirs } = n2
const oldProps = n1.props || EMPTY_OBJ
const newProps = n2.props || EMPTY_OBJ
let vnodeHook: VNodeHook | undefined | null
// toggleRecurse作用是不允许递归beforeUpdate hooks
parentComponent && toggleRecurse(parentComponent, false)
if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
invokeVNodeHook(vnodeHook, parentComponent, n2, n1)
}
// 触发beforeUpdate事件
if (dirs) {
invokeDirectiveHook(n2, n1, parentComponent, 'beforeUpdate')
}
// 解锁
parentComponent && toggleRecurse(parentComponent, true)
if (dynamicChildren) {
// 动态属性节点的处理
// 使用dynamicChildren来直接处理
patchBlockChildren(/* 忽略参数 */)
} else if (!optimized) {
// 没有经过optimized优化的,全量diff
patchChildren(/* 忽略参数 */)
}
if (patchFlag > 0) {
// 针对优化过的vnode,进行靶向更新
if (patchFlag & PatchFlags.FULL_PROPS) {
// 如果是动态key节点,需要全量对比
patchProps(/* 忽略参数 */)
} else {
if (patchFlag & PatchFlags.CLASS) {
// 动态calss节点,那么需要进行class比较
if (oldProps.class !== newProps.class) {
hostPatchProp(/* 忽略参数 */)
}
}
// style
// 动态style节点,那么需要进行class比较
if (patchFlag & PatchFlags.STYLE) {
hostPatchProp(/* 忽略参数 */)
}
//
// 动态属性节点
if (patchFlag & PatchFlags.PROPS) {
// 变量后面加!是typescript的非空断言
// dynamicProps的含义跟dynamicChild一样,是编辑动态传入的prop
// 只需对比dynamicProps,不用全比对,增快性能
const propsToUpdate = n2.dynamicProps!
for (let i = 0; i < propsToUpdate.length; i++) {
const key = propsToUpdate[i]
const prev = oldProps[key]
const next = newProps[key]
if (next !== prev || key === 'value') {
hostPatchProp(/* 忽略参数 */)
}
}
}
}
// 当有动态text文本类型,则进行对比,文本类型相对简单,直接覆盖即可
if (patchFlag & PatchFlags.TEXT) {
if (n1.children !== n2.children) {
hostSetElementText(/* 忽略参数 */)
}
}
} else if (!optimized && dynamicChildren == null) {
// 没有优化则全量对比
patchProps(/* 忽略参数 */)
}
// 如果有绑定钩子函数,那么将通过queuePostRenderEffect的方式,在组件渲染完毕后,执行节点updated
if ((vnodeHook = newProps.onVnodeUpdated) || dirs) {
queuePostRenderEffect(() => {
vnodeHook && invokeVNodeHook(/* 忽略参数 */)
dirs && invokeDirectiveHook(/* 忽略参数 */)
}, parentSuspense)
}
}
流程:
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds;
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 对于已经被keepAlive缓存的组件做处理
; (parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
// 渲染组件
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
} else {
// 更新组件
updateComponent(n1, n2, optimized)
}
}
// 更新组件
const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => {
const instance = (n2.component = n1.component)!;
// 是否需要更新
if (shouldUpdateComponent(n1, n2, optimized)) {
if (
__FEATURE_SUSPENSE__ &&
instance.asyncDep &&
!instance.asyncResolved
) {
// 尚未加载异步组件只需要更新子组件和props即可
updateComponentPreRender(instance, n2, optimized)
return
} else {
// next代表此次即将更新的vnode
instance.next = n2
// 检查更新队列是否有现组件,有则移除出队列,避免子组件多次更新
invalidateJob(instance.update)
// 执行更新
instance.update()
}
} else {
// 做一次copy达到复用
n2.component = n1.component
n2.el = n1.el
instance.vnode = n2
}
}
注:patchKeyedChildren函数较复杂,下文会单独详解
流程:
下面是流程伪代码
if (有标签动态特性) {
// 有使用自定义组件外部使用了key进行列表渲染
if (是否为v-for并且有绑定key的节点) {
diff优化对比
return
} else if (是否为v-for并且没有绑定key的节点) {
硬对比
return
}
}
if (旧节点是文本) {
if (新节点是数组) {
卸载删除
}
if (文本不相等) {
替换即可
}
} else {
if (旧节点是数组) {
if (新节点是数组) {
diff优化对比
} else 新节点不为数组 {
删除旧的
}
} else 旧节点是文本 {
if (新节点非文本类型) {
置空旧节点
}
if (到这里旧节点就是空) {
新增挂载新节点
}
}
}
const patchChildren: PatchChildrenFn = (
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized = false
) => {
const c1 = n1 && n1.children
const prevShapeFlag = n1 ? n1.shapeFlag : 0
const c2 = n2.children
// patchFlag代表此元素里部分属性是动态的,非静态,在diff时只会对patchFlag>0的值做对比
const { patchFlag, shapeFlag } = n2;
if (patchFlag > 0) {
// v-for子节点处理
if (patchFlag & PatchFlags.KEYED_FRAGMENT) {
// 有设置key数组子元素对比
// 当一组节点中有些有有些没有key,可会走到这里
patchKeyedChildren(/* 忽略参数 */)
return
} else if (patchFlag & PatchFlags.UNKEYED_FRAGMENT) {
// unkeyed
// 没有key属性的元素对比
// 直接对比新旧相同长度的部分(patch),删除减少的部分(unmountChildren),直接增加比旧长度长的部分(mountChildren)
patchUnkeyedChildren(/* 忽略参数 */)
return
}
}
// 对于子节点来说,有三种可能:文本、数组、空
// 文本节点处理
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// 如果新节点为文本类型,旧节点为数组类型,则删除旧节点
// text children fast path
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(/* 忽略参数 */)
}
// 如果新旧直接不一样则直接覆盖旧内容
// 一样就不用,因为是文本 stirng
if (c2 !== c1) {
hostSetElementText(/* 忽略参数 */)
}
} else {
// 普通数组列表节点处理
if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// 如果新旧节点都为数组列表,则开始对比diff流程
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
patchKeyedChildren(/* 忽略参数 */)
} else {
// 如果新节点不为数组,则删除旧节点
unmountChildren(/* 忽略参数 */)
}
} else {
// 旧节点为文本类型,新节点非文本类型,则把vnode文本置为空
if (prevShapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(container, '')
}
// 旧节点非数组列表(到这里已经代表等于空了),新节点为数组列表,则直接新增挂载
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(/* 忽略参数 */)
}
}
}
}
这里就是vNode节点diff的函数,令无数前端面试者倒在这道题上,这里也会做详解,并画图解释流程。先看下源码部分
先解释一部分函数
export function cloneIfMounted(child: VNode): VNode {
return child.el === null || child.memo ? child : cloneVNode(child)
}
export function normalizeVNode(child: VNodeChild): VNode {
// 空或boolean:创建一个空的注释节点
if (child == null || typeof child === 'boolean') {
// empty placeholder
return createVNode(Comment)
} else if (isArray(child)) {
// 数组列表,创建一个Fragment把Vnode包起来
return createVNode(
Fragment,
null,
// #3666, avoid reference pollution when reusing vnode
child.slice()
)
} else if (typeof child === 'object') {
// 对象使用cloneIfMounted处理
// already vnode, this should be the most common since compiled templates
// always produce all-vnode children arrays
return cloneIfMounted(child)
} else {
// strings and numbers
// 其他当做文本节点处理
return createVNode(Text, null, String(child))
}
}
// 对比新旧节点的tag(标签名)以及传入的key是否相同
export function isSameVNodeType(n1: VNode, n2: VNode): boolean {
return n1.type === n2.type && n1.key === n2.key
}
源码:
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
let e2 = l2 - 1
// 1、从前(头)开始进行对比
while (i <= e1 && i <= e2) {
const n1 = c1[i]
// 是否能优化重用vnode
const n2 = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
// 新旧节点的tag(标签名)以及传入的key是否相同
if (isSameVNodeType(n1, n2)) {
patch(/* 忽略参数 */)
} else {
break
}
i++
}
// 2、从后(尾)开始进行对比
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(/* 忽略参数 */)
} else {
break
}
e1--
e2--
}
// 前后遍历完,只剩中间部分不同的部分
if (i > e1) {
// 3、对于简单的左右两边新增:左边或右边有新增的数据
// (在只有一边的情况下才会走进这里)
// 简单的往右边push新增数据,从前开始遍历的while就能完成对比
// (a b)
// (a b) c
// i = 2, e1 = 1, e2 = 2
// 简单往左边unshift新增数据,从后开始遍历的while就能完成对比
// (a b)
// c (a b)
// i = 0, e1 = -1, e2 = 0
if (i <= e2) {
// 插入index
const nextPos = e2 + 1;
// 插入基准vnode
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])),
/* 忽略参数 */
)
i++
}
}
} else if (i > e2) {
// 4、对于简单的左右两边删除:左边或右边有删除的数据
// (在只有一边的情况下才会走进这里)
// 简单的往右边删除n个节点,从前开始遍历的while就能完成对比
// (a b) c
// (a b)
// i = 2, e1 = 2, e2 = 1
// 简单的往左边push n个节点,从前开始遍历的while就能完成对比
// a (b c)
// (b c)
// i = 0, e1 = 0, e2 = -1
while (i <= e1) {
// 那么删除旧节点
unmount(/* 忽略参数 */)
i++
}
} else {
// 5、剩下的就是中间一块不确定,是否新增 or 删除 or 复用
// 如果新旧头尾都不一样,也会是整个列表进行对比。
// 例子1:新比旧长
// [i ... e1 + 1]: a b [c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 4, e2 = 5
// 例子2:新比旧短
// [i ... e1 + 1]: a b [c d e z l] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 6, e2 = 5
// 例子3: 纯变换位置
// [i ... e1 + 1]: a b [h c d e] f g
// [i ... e2 + 1]: a b [e d c h] f g
// i = 2, e1 = 5, e2 = 5
const s1 = i // prev starting index
const s2 = i // next starting index
// 5.1 将不确定操作的新节点部分放进map,已设置的key作为map的key
const keyToNewIndexMap: Map<string | number | symbol, number> = new Map();
for (i = s2; i <= e2; i++) {
const nextChild = (c2[i] = optimized
? cloneIfMounted(c2[i] as VNode)
: normalizeVNode(c2[i]))
if (nextChild.key != null) {
keyToNewIndexMap.set(nextChild.key, i)
}
}
// 5.2 对比中间不确定操作的节点
let j
let patched = 0
// 计算新节点中还有几个节点需要被patched
const toBePatched = e2 - s2 + 1
// 是否需要移动标记
let moved = false
// 记录这次对比的旧节点到新节点最长可复用到哪里
// 比如例子一maxNewIndexSoFar会等于4
let maxNewIndexSoFar = 0
// 这是一个新节点对应旧节点的数组 index = 新节点需要对比的集合Index value = 旧节点index + 1
// 做最长递增子序列时使用
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) {
// 如果比对的时候i超过了新节点需要对比的部分(需要对比的新节点比旧节点短),则删除旧节点
// 比如 old [a, b, c ,d, e] new [ a, c, b, e] 旧节点的 d是此时需要unmount的节点
unmount(prevChild, parentComponent, parentSuspense, true)
continue
}
let newIndex
if (prevChild.key != null) {
// 旧节点key 是否有在新节点找到
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) {
// 如果旧节点对应的新节点map里面找不到,则代表不存在,卸载删除
unmount(prevChild, parentComponent, parentSuspense, true)
} else {
// 如果旧节点在新节点中能复用,则记录下来
newIndexToOldIndexMap[newIndex - s2] = i + 1
if (newIndex >= maxNewIndexSoFar) {
maxNewIndexSoFar = newIndex
} else {
moved = true
}
// 对比新旧变化
patch(/* 忽略参数 */)
patched++
}
}
// 5.3 如果新节点有旧节点移动的迹象,拿到newIndexToOldIndexMap计算最长递增子序列
// 这样可以操作最少的dom来更新
// getSequence返回的是传入数组的index
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
// 到这里剩下的只有三种情况, 该删除的节点前面逻辑已经删掉了
// 1、节点位置没有变,只是被夹在改变的数据中
// 2、节点改变了位置
// 3、新增节点
if (newIndexToOldIndexMap[i] === 0) {
// 如果新节点的元素在旧节点里面没有,代表不可复用
// 创建并挂载新节点
patch(
null,
nextChild,
/* 忽略参数 */
)
} else if (moved) {
if (j < 0 || i !== increasingNewIndexSequence[j]) {
// 如果不属于最佳子序列中,则代表可以移动的元素
move(nextChild, container, anchor, MoveType.REORDER)
} else {
// 属于最佳子序列中,则什么都不用动,留着此节点复用
j--
}
}
}
}
}
大致流程:
此函数逻辑分支较多,常作为面试题,下面将会以图的形式贴上几个场景的流程;
1、往尾部push插入元素,常见场景如分页插入
那么在第一个if判断 if (i > e1) 已经知道新的比旧的长并且是往右边插入,那么只需创建挂载新的节点即可
2、往尾部插入两个元素
如果是这种情况,第一个while对比不会运行,直接退出,在第二个while对比时,e1根据遍历会慢慢减到-1,最终进入 if (i > e1) 中,只需创建挂载新的节点即可
3、删除头部两个元素
如果是删除左边,第一个while对比不会运行,直接退出,在第二个while对比时,e1根据遍历会慢慢减到1,最终进入else if (i > e2) 中,循环遍历调用unmount卸载删除节点
4、头尾部分元素相同,中间部分节点变换位置;
假设a-p列表元素都有key属性,新的列表 d、I、M换了位置;
第一步,头和尾的对比
走到代码的else逻辑,此时提取出还未对比的部分为keyToNewIndexMap,再创建新节点对应旧节点的index—newIndexToOldIndexMap,此时keyToNewIndexMap和newIndexToOldIndexMap的值,判断到有移动的操作,此时moved变量为true
// 新列表key对应的index
keyToNewIndexMap = {
'm': 3,
'e': 4,
'f': 5,
'g': 6,
'h': 7,
'd': 8,
'j': 9,
'k': 10,
'l': 11,
'i': 12
}
//新列表节点的Index(减去已经对比的元素长度)对应旧节点的index位置(原位置)
newIndexToOldIndexMap = [ 13, 5, 6, 7, 8, 4, 10, 11, 12, 9]
那么接下来只需调用move去移动元素就可以了
首先会计算出newIndexToOldIndexMap 的最长递增子序列
getSequence([ 13, 5, 6, 7, 8, 4, 10, 11, 12, 9])
结果(返回传入数组的index): [1, 2, 3, 4, 6, 7, 8]
那么此时需要移动的就是下标为:0、5、9,对应的就是d、i、m
至此,此次对比完成
5、头尾部分元素相同,中间节点包括移动、新增、删除操作
删除(灰色):l、m
移动(其他彩色):e、f、i、j
新增(红色):x
第一步头尾对比就直接忽略,前面四个已经说清楚了,这里就不在重复了
从keyToNewIndexMap这行 开始
新列表key对应的index
keyToNewIndexMap = {
'i': 4,
'j': 5,
'g': 6,
'h': 7,
'f': 8,
'e': 9,
'k': 10,
'x': 11,
}
// 此时
l2 = 15
e1 = 12
e2 = 11
i = 4
// 需要对比的节点长度
toBePatched = e2 - s2 + 1 = 11 - 4 + 1 = 8;
然后创建newIndexToOldIndexMap,并做删除与新增流程
开始遍历旧的去掉头尾已经对比完剩下的不相同节点,尝试找出可以重用的地方,发现除了I、m,都可重用,并且有移动的节点,此时moved标记激活为true。
I、m两个节点也是在这一步被删除了
// 得出
newIndexToOldIndexMap = [ 9, 10, 7, 8, 6, 5, 11, 0]
往下走调用getSequence计算出newIndexToOldIndexMap 的最长递增子序列
//得出
getSequence([ 9, 10, 7, 8, 6, 5, 11, 0])
[2, 3, 6]
代表着除了下标2、3、6的元素,其余都需要去移动
从尾部开始遍历newIndexToOldIndexMap,如果value为0,则代表需要新增节点,那么创建挂载,如果不属于最佳子序列结果中的下标元素,则移动;
执行后,此次对比完成
本文简单介绍了patch的过程,随着源码讲解了vue3对于patch过程的很多优化,希望能帮助读者了解vue的渲染过程。
欢迎大家提出疑问和问题,谢谢!