在上文中,我们分析了组件级别的生命周期钩子。本文我们将分析 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);
}
};
执行流程
-
onVnodeBeforeMount:
- 在 DOM 元素创建后、插入前同步执行
- 可以访问到已创建但未插入的 DOM 元素
- 适合进行 DOM 属性的预处理
-
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);
}
};
执行流程
-
onVnodeBeforeUpdate:
- 在节点更新前同步执行
- 可以访问更新前后的 props 和 DOM 元素
- 适合保存更新前的状态
-
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);
}
};
执行流程
-
onVnodeBeforeUnmount:
- 在节点移除前同步执行
- DOM 元素仍在文档中
- 适合执行清理工作
-
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 钩子的执行顺序:
创建过程
- onVnodeBeforeMount (同步)
- onVnodeMounted (异步)
更新过程
- onVnodeBeforeUpdate (同步)
- onVnodeUpdated (异步)
卸载过程
- onVnodeBeforeUnmount (同步)
- onVnodeUnmounted (异步)
特点:
- VNode 钩子提供了比组件生命周期更细粒度的控制
- 同步钩子在 DOM 操作前执行,用于准备工作
- 异步钩子在 DOM 操作后执行,用于处理副作用
- 所有钩子都能访问到实际的 DOM 元素(vnode.el)
- 更新钩子可以访问到更新前后的 VNode 状态
- 适合处理需要直接操作 DOM 的场景
- 常用于集成第三方库或实现特殊的 DOM 处理逻辑
这种设计让开发者能够在 DOM 操作的各个阶段进行精确控制,同时通过同步/异步执行的区分来保证操作的安全性和性能。