第 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 版权所有
