vue3源码解析:虚拟节点生命周期

131 阅读4分钟

在上文中,我们分析了组件级别的生命周期钩子。本文我们将分析 Vue3 中虚拟节点(VNode)级别的生命周期钩子,这些钩子提供了更细粒度的 DOM 操作控制。

1. VNode 钩子函数的执行时机

Vue3 为虚拟节点提供了以下生命周期钩子:

创建阶段

  • onVnodeBeforeMount: 节点挂载到 DOM 之前
  • onVnodeMounted: 节点挂载到 DOM 之后

更新阶段

  • onVnodeBeforeUpdate: 节点更新之前
  • onVnodeUpdated: 节点更新之后

卸载阶段

  • onVnodeBeforeUnmount: 节点卸载之前
  • onVnodeUnmounted: 节点卸载之后

2. 创建阶段

源码分析

// renderer.ts -> mountElement 函数
const mountElement = (
  vnode: VNode,
  container: RendererElement,
  anchor: RendererNode | null,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  namespace: ElementNamespace,
  optimized: boolean
) => {
  let el: RendererElement;
  let vnodeHook: VNodeHook | undefined | null;
  const { props, shapeFlag, transition, dirs } = vnode;

  // 1. 创建 DOM 元素
  el = vnode.el = hostCreateElement(vnode.type as string, namespace);

  // 2. 处理子节点...

  // 3. 执行 onVnodeBeforeMount 钩子
  if ((vnodeHook = props && props.onVnodeBeforeMount)) {
    invokeVNodeHook(vnodeHook, parentComponent, vnode);
  }

  // 4. 插入 DOM
  hostInsert(el, container, anchor);

  // 5. 执行 onVnodeMounted 钩子(异步)
  if ((vnodeHook = props && props.onVnodeMounted)) {
    queuePostRenderEffect(() => {
      invokeVNodeHook(vnodeHook!, parentComponent, vnode);
    }, parentSuspense);
  }
};

执行流程

  1. onVnodeBeforeMount:

    • 在 DOM 元素创建后、插入前同步执行
    • 可以访问到已创建但未插入的 DOM 元素
    • 适合进行 DOM 属性的预处理
  2. onVnodeMounted:

    • 在 DOM 元素插入后异步执行
    • 确保元素已完全挂载到文档中
    • 可以安全地进行 DOM 操作

使用示例

// 在模板中使用
<template>
  <div
    :onVnodeBeforeMount="handleBeforeMount"
    :onVnodeMounted="handleMounted"
  >
    {{ content }}
  </div>
</template>

<script>
export default {
  setup() {
    const handleBeforeMount = (vnode) => {
      // 1. 访问创建的 DOM 元素
      console.log('DOM 元素:', vnode.el)

      // 2. 添加自定义属性
      vnode.el.customData = { /* ... */ }
    }

    const handleMounted = (vnode) => {
      // 1. 初始化第三方库
      new ThirdPartyLib(vnode.el)

      // 2. 添加额外的事件监听
      vnode.el.addEventListener('custom-event', handler)
    }

    return {
      handleBeforeMount,
      handleMounted
    }
  }
}
</script>

3. 更新阶段

源码分析

// renderer.ts -> patchElement 函数
const patchElement = (
  n1: VNode,
  n2: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | 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;

  // 1. 执行 onVnodeBeforeUpdate 钩子
  if ((vnodeHook = newProps.onVnodeBeforeUpdate)) {
    invokeVNodeHook(vnodeHook, parentComponent, n2, n1);
  }

  // 2. 更新元素...
  if (patchFlag > 0) {
    // 更新 props、class、style 等
  }

  // 3. 执行 onVnodeUpdated 钩子(异步)
  if ((vnodeHook = newProps.onVnodeUpdated)) {
    queuePostRenderEffect(() => {
      invokeVNodeHook(vnodeHook!, parentComponent, n2, n1);
    }, parentSuspense);
  }
};

执行流程

  1. onVnodeBeforeUpdate:

    • 在节点更新前同步执行
    • 可以访问更新前后的 props 和 DOM 元素
    • 适合保存更新前的状态
  2. onVnodeUpdated:

    • 在节点更新后异步执行
    • DOM 已完成更新
    • 可以根据新状态执行副作用

使用示例

<template>
  <div
    :onVnodeBeforeUpdate="handleBeforeUpdate"
    :onVnodeUpdated="handleUpdated"
  >
    {{ content }}
  </div>
</template>

<script>
export default {
  setup() {
    const handleBeforeUpdate = (newVNode, oldVNode) => {
      // 1. 保存更新前的状态
      const prevScroll = oldVNode.el.scrollTop
      newVNode.el._prevScroll = prevScroll

      // 2. 比较前后变化
      console.log('props 变化:', oldVNode.props, '->', newVNode.props)
    }

    const handleUpdated = (newVNode, oldVNode) => {
      // 1. 恢复滚动位置
      if (newVNode.el._prevScroll) {
        newVNode.el.scrollTop = newVNode.el._prevScroll
      }

      // 2. 更新关联的第三方库
      if (newVNode.el.chart) {
        newVNode.el.chart.update()
      }
    }

    return {
      handleBeforeUpdate,
      handleUpdated
    }
  }
}
</script>

4. 卸载阶段

源码分析

// renderer.ts -> unmount 函数
const unmount = (
  vnode: VNode,
  parentComponent: ComponentInternalInstance | null,
  parentSuspense: SuspenseBoundary | null,
  doRemove?: boolean
) => {
  const { props, ref, children } = vnode;
  let vnodeHook: VNodeHook | undefined | null;

  // 1. 执行 onVnodeBeforeUnmount 钩子
  if ((vnodeHook = props && props.onVnodeBeforeUnmount)) {
    invokeVNodeHook(vnodeHook, parentComponent, vnode);
  }

  // 2. 移除 DOM 元素
  if (doRemove) {
    remove(vnode);
  }

  // 3. 执行 onVnodeUnmounted 钩子(异步)
  if ((vnodeHook = props && props.onVnodeUnmounted)) {
    queuePostRenderEffect(() => {
      invokeVNodeHook(vnodeHook!, parentComponent, vnode);
    }, parentSuspense);
  }
};

执行流程

  1. onVnodeBeforeUnmount:

    • 在节点移除前同步执行
    • DOM 元素仍在文档中
    • 适合执行清理工作
  2. onVnodeUnmounted:

    • 在节点移除后异步执行
    • DOM 元素已从文档中移除
    • 适合执行最终的清理工作

使用示例

<template>
  <div
    v-if="show"
    :onVnodeBeforeUnmount="handleBeforeUnmount"
    :onVnodeUnmounted="handleUnmounted"
  >
    {{ content }}
  </div>
</template>

<script>
export default {
  setup() {
    const handleBeforeUnmount = (vnode) => {
      // 1. 保存状态
      localStorage.setItem('temp-state', JSON.stringify({
        scroll: vnode.el.scrollTop,
        data: vnode.el.dataset
      }))

      // 2. 清理事件监听
      vnode.el.removeEventListener('custom-event', handler)
    }

    const handleUnmounted = (vnode) => {
      // 1. 销毁关联的实例
      if (vnode.el.chart) {
        vnode.el.chart.destroy()
      }

      // 2. 清理引用
      delete vnode.el._instance
    }

    return {
      handleBeforeUnmount,
      handleUnmounted
    }
  }
}
</script>

5. 总结

VNode 钩子的执行顺序:

创建过程

  1. onVnodeBeforeMount (同步)
  2. onVnodeMounted (异步)

更新过程

  1. onVnodeBeforeUpdate (同步)
  2. onVnodeUpdated (异步)

卸载过程

  1. onVnodeBeforeUnmount (同步)
  2. onVnodeUnmounted (异步)

特点:

  1. VNode 钩子提供了比组件生命周期更细粒度的控制
  2. 同步钩子在 DOM 操作前执行,用于准备工作
  3. 异步钩子在 DOM 操作后执行,用于处理副作用
  4. 所有钩子都能访问到实际的 DOM 元素(vnode.el)
  5. 更新钩子可以访问到更新前后的 VNode 状态
  6. 适合处理需要直接操作 DOM 的场景
  7. 常用于集成第三方库或实现特殊的 DOM 处理逻辑

这种设计让开发者能够在 DOM 操作的各个阶段进行精确控制,同时通过同步/异步执行的区分来保证操作的安全性和性能。