第 32 期 - Vue3 虚拟 DOM 更新机制剖析
logoFRONTALK AI/11月24日 16:32/阅读原文

摘要

文章围绕 Vue3 虚拟 DOM 的更新机制展开,介绍了从数据变化触发更新到最终处理 HTML 元素的流程,包括相关函数的作用和节点更新的不同情况等。

一、整体流程概述

文章首先给出一个 Vue 示例,其中包含数据更新的操作。在 Vue3 中,当数据发生变化时,会触发setupRenderEffectupdate方法,然后执行patch操作。这个patch操作会不断深入,从更新组件与子树(patch方法中的processComponent),到最终调用patchElement处理 HTML 元素,这个过程涉及到多个函数的调用和复杂的逻辑判断。

二、虚拟 DOM 相关函数

(一)虚拟 DOM 的创建

  1. packages/runtime - core/src/vnode.ts中,有一系列创建虚拟 DOM 节点(vnode)的函数,如createVNode_createVNodecreateBaseVNode等。
    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等)。
  2. 还有针对特定类型vnode的创建函数,如createTextVNodecreateStaticVNodecreateCommentVNode等,它们在不同的场景下创建对应的vnode

(二)setupRenderEffect函数

  1. 在首次渲染时,会走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();
    }
    
  2. 在非首次渲染时,还会涉及到updateComponentPreRenderer函数用于更新solt和属性,renderComponentRoot函数用于创建子树。

三、patch相关操作

(一)patch函数

  1. patch函数是更新流程中的核心函数,当n1n2相同时直接返回,若类型不同且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;
      }
      // 其他根据不同类型进行处理的逻辑
    }
    
  2. 根据n2的类型(如TextCommentStaticFragment等)分别进行不同的处理,对于ELEMENTCOMPONENT类型,会调用processElementprocessComponent函数。

(二)processComponent函数

  1. n1null时,如果n2ShapeFlags.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函数

  1. 首先判断是否需要更新(shouldUpdateComponent),如果需要更新则设置instance.next = n2,标记instance.effect.dirty = true并执行instance.update(),这会再次进入setupRenderEffectpatch子树,不断递归直到处理 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函数整体

  1. 这个函数主要处理两种情况:子节点更新和节点自身更新。
    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;
      // 一系列处理逻辑
    }
    

(二)子节点更新

  1. 在子节点更新时,如果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
      );
    }
    
  2. patchChildren函数中,根据patchFlag以及新旧子节点的类型(文本、数组、空)进行不同的处理,最复杂的是新旧子节点都是数组的情况,有标记key时使用patchKeyedChildren,没有则使用patchUnkeyedChildren

(三)节点自身更新

  1. 根据patchFlag的值进行不同的属性比较操作,如FULL_PROPSCLASSSTYLEPROPSTEXT等情况的处理,当patchFlag不满足且dynamicChildrennull时进行全量属性对比(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 版权所有