第 32 期 - Vue3 虚拟 DOM 更新机制剖析
摘要
文章围绕 Vue3 虚拟 DOM 的更新机制展开,介绍了从数据变化触发更新到最终处理 HTML 元素的流程,包括相关函数的作用和节点更新的不同情况等。
一、整体流程概述
文章首先给出一个 Vue 示例,其中包含数据更新的操作。在 Vue3 中,当数据发生变化时,会触发setupRenderEffect
的update
方法,然后执行patch
操作。这个patch
操作会不断深入,从更新组件与子树(patch
方法中的processComponent
),到最终调用patchElement
处理 HTML 元素,这个过程涉及到多个函数的调用和复杂的逻辑判断。
二、虚拟 DOM 相关函数
(一)虚拟 DOM 的创建
- 在
packages/runtime - core/src/vnode.ts
中,有一系列创建虚拟 DOM 节点(vnode
)的函数,如createVNode
、_createVNode
和createBaseVNode
等。
这些函数用于构建不同类型的function _createVNode( type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, props: (Data & VNodeProps) | null = null, children: unknown = null, patchFlag: number = 0, dynamicProps: string[] | null = null, isBlockNode = false ): VNode { return createBaseVNode( type, props, children, patchFlag, dynamicProps, shapeFlag, isBlockNode, true ); }
vnode
,根据传入的类型、属性、子节点等信息进行创建,并设置相关的标志位(patchFlag
等)。 - 还有针对特定类型
vnode
的创建函数,如createTextVNode
、createStaticVNode
和createCommentVNode
等,它们在不同的场景下创建对应的vnode
。
(二)setupRenderEffect
函数
- 在首次渲染时,会走
patch(null, subTree,...)
,而在非首次渲染时,会进行自身更新和创建子树的操作。const setupRenderEffect: SetupRenderEffectFn = ( instance, initialVNode, container, anchor, parentSuspense, namespace: ElementNamespace, optimized ) => { const componentUpdateFn = () => { if (!instance.isMounted) { // 首次渲染相关逻辑 if (el && hydrateNode) { } else { patch(null, subTree, container, anchor, instance, parentSuspense, namespace); initialVNode.el = subTree.el; } instance.isMounted = true; } else { // 非首次渲染相关逻辑 const nextTree = renderComponentRoot(instance); const prevTree = instance.subTree; instance.subTree = nextTree; patch(prevTree, nextTree, hostParentNode(prevTree.el!)!, getNextHostNode(prevTree), instance, parentSuspense, namespace); next.el = nextTree.el; } }; // 创建响应式效果相关逻辑 const effect = (instance.effect = new ReactiveEffect( componentUpdateFn, NOOP, () => queueJob(update), instance.scope )); const update: SchedulerJob = (instance.update = () => { if (effect.dirty) { effect.run(); } }); update.id = instance.uid; update(); }
- 在非首次渲染时,还会涉及到
updateComponentPreRenderer
函数用于更新solt
和属性,renderComponentRoot
函数用于创建子树。
三、patch
相关操作
(一)patch
函数
patch
函数是更新流程中的核心函数,当n1
和n2
相同时直接返回,若类型不同且n1
存在则卸载旧树并将n1
设为null
。const patch: PatchFn = ( n1, n2, container, anchor = null, parentComponent = null, parentSuspense = null, namespace = undefined, slotScopeIds = null, optimized = __DEV__ && isHmrUpdating? false :!!n2.dynamicChildren ) => { if (n1 === n2) { return; } if (n1 &&!isSameVNodeType(n1, n2)) { anchor = getNextHostNode(n1); unmount(n1, parentComponent, parentSuspense, true); n1 = null; } // 其他根据不同类型进行处理的逻辑 }
- 根据
n2
的类型(如Text
、Comment
、Static
、Fragment
等)分别进行不同的处理,对于ELEMENT
和COMPONENT
类型,会调用processElement
和processComponent
函数。
(二)processComponent
函数
- 当
n1
为null
时,如果n2
是ShapeFlags.COMPONENT_KEPT_ALIVE
类型则激活,否则挂载组件;当n1
不为null
时,调用updateComponent
函数进行更新。const processComponent = ( n1: VNode | null, n2: VNode, container: RendererElement, anchor: RendererNode | null, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { n2.slotScopeIds = slotScopeIds; if (n1 == null) { if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) { (parentComponent!.ctx as KeepAliveContext).activate( n2, container, anchor, namespace, optimized ); } else { mountComponent( n2, container, anchor, parentComponent, parentSuspense, namespace, optimized ); } } else { updateComponent(n1, n2, optimized); } }
(三)updateComponent
函数
- 首先判断是否需要更新(
shouldUpdateComponent
),如果需要更新则设置instance.next = n2
,标记instance.effect.dirty = true
并执行instance.update()
,这会再次进入setupRenderEffect
的patch
子树,不断递归直到处理 HTML 内容;如果不需要更新则直接复制属性。const updateComponent = (n1: VNode, n2: VNode, optimized: boolean) => { const instance = (n2.component = n1.component)!; if (shouldUpdateComponent(n1, n2, optimized)) { instance.next = n2; invalidateJob(instance.update); instance.effect.dirty = true; instance.update(); } else { n2.el = n1.el; instance.vnode = n2; } }
四、patchElement
操作
(一)patchElement
函数整体
- 这个函数主要处理两种情况:子节点更新和节点自身更新。
const patchElement = ( n1: VNode, n2: VNode, parentComponent: ComponentInternalInstance | null, parentSuspense: SuspenseBoundary | null, namespace: ElementNamespace, slotScopeIds: string[] | null, optimized: boolean ) => { const el = (n2.el = n1.el!); let { patchFlag, dynamicChildren, dirs } = n2; // 一系列处理逻辑 }
(二)子节点更新
- 在子节点更新时,如果
dynamicChildren
存在则调用patchBlockChildren
,否则如果未优化则进行全量对比(patchChildren
)。if (dynamicChildren) { patchBlockChildren( n1.dynamicChildren!, dynamicChildren, el, parentComponent, parentSuspense, resolveChildrenNamespace(n2, namespace), slotScopeIds ); } else if (!optimized) { patchChildren( n1, n2, el, null, parentComponent, parentSuspense, resolveChildrenNamespace(n2, namespace), slotScopeIds, false ); }
- 在
patchChildren
函数中,根据patchFlag
以及新旧子节点的类型(文本、数组、空)进行不同的处理,最复杂的是新旧子节点都是数组的情况,有标记key
时使用patchKeyedChildren
,没有则使用patchUnkeyedChildren
。
(三)节点自身更新
- 根据
patchFlag
的值进行不同的属性比较操作,如FULL_PROPS
、CLASS
、STYLE
、PROPS
和TEXT
等情况的处理,当patchFlag
不满足且dynamicChildren
为null
时进行全量属性对比(patchProps
)。if (patchFlag > 0) { if (patchFlag & PatchFlags.FULL_PROPS) { patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, namespace ); } else { if (patchFlag & PatchFlags.CLASS) { if (oldProps.class!== newProps.class) { hostPatchProp(el, 'class', null, newProps.class, namespace); } } // 其他属性处理逻辑 } } else if (!optimized && dynamicChildren == null) { patchProps( el, n2, oldProps, newProps, parentComponent, parentSuspense, namespace ); }
扩展阅读
Made by 捣鼓键盘的小麦 / © 2025 Front Talk 版权所有