{T}

应用实例与组件实例

Vue 3 将应用实例和组件实例分离,是一个重要的架构改进。Vue 3.5+ 优化了实例创建性能和内存占用。理解两者的区别和关联,对于掌握 Vue 3 至关重要。

应用实例与组件实例的关系

code
┌────────────────────────────────────────────────────────────┐
│                    实例层级架构                             │
├────────────────────────────────────────────────────────────┤
│                                                            │
│   ┌─────────────────────────────────────────────────────┐ │
│   │                   应用实例 (App)                     │ │
│   │                                                     │ │
│   │  - createApp() 创建                                 │ │
│   │  - 全局配置、插件、组件注册                          │ │
│   │  - 独立的上下文环境                                 │ │
│   │                                                     │ │
│   └─────────────────────┬───────────────────────────────┘ │
│                         │                                  │
│                         │ mount()                          │
│                         │                                  │
│                         ▼                                  │
│   ┌─────────────────────────────────────────────────────┐ │
│   │                 根组件实例 (Root)                    │ │
│   │                                                     │ │
│   │  - 应用实例挂载后创建                               │ │
│   │  - 组件树的起点                                     │ │
│   │                                                     │ │
│   └─────────────────────┬───────────────────────────────┘ │
│                         │                                  │
│          ┌──────────────┼──────────────┐                  │
│          ▼              ▼              ▼                  │
│   ┌───────────┐  ┌───────────┐  ┌───────────┐            │
│   │ 子组件实例 │  │ 子组件实例 │  │ 子组件实例 │            │
│   │   (A)     │  │   (B)     │  │   (C)     │            │
│   └─────┬─────┘  └─────┬─────┘  └───────────┘            │
│         │              │                                  │
│         ▼              ▼                                  │
│   ┌───────────┐  ┌───────────┐                           │
│   │ 孙组件实例 │  │ 孙组件实例 │                           │
│   │  (A-1)    │  │  (B-1)    │                           │
│   └───────────┘  └───────────┘                           │
│                                                            │
└────────────────────────────────────────────────────────────┘

应用实例

创建应用实例

js
import { createApp } from 'vue'
import App from './App.vue'

// 创建应用实例
const app = createApp(App)

// 挂载到 DOM
app.mount('#app')

应用实例完整 API

js
import { createApp } from 'vue'

const app = createApp({
  // 根组件选项
})

// ===== 注册全局组件 =====
app.component('MyButton', {
  template: '<button>Click</button>'
})

// 注册多个组件
app.component('ComponentA', ComponentA)
app.component('ComponentB', ComponentB)

// 获取已注册组件
const MyComponent = app.component('MyComponent')

// ===== 注册全局指令 =====
app.directive('focus', {
  mounted(el) {
    el.focus()
  }
})

// 简写形式(mounted + updated)
app.directive('color', (el, binding) => {
  el.style.color = binding.value
})

// ===== 注册插件 =====
import { createRouter } from 'vue-router'
import { createPinia } from 'pinia'

const router = createRouter({ /* ... */ })
const pinia = createPinia()

app.use(router)
app.use(pinia)
app.use(MyPlugin, { /* 插件选项 */ })

// ===== 全局配置 =====
// 全局属性(谨慎使用)
app.config.globalProperties.$http = axios
app.config.globalProperties.$format = formatFunction

// 错误处理器
app.config.errorHandler = (err, instance, info) => {
  console.error('Global error:', err)
  console.log('Component:', instance)
  console.log('Error info:', info)
}

// 警告处理器
app.config.warnHandler = (msg, instance, trace) => {
  console.warn('Warning:', msg)
}

// 性能追踪
app.config.performance = true

// 编译选项
app.config.compilerOptions = {
  isCustomElement: tag => tag.startsWith('my-'),
  whitespace: 'condense',
  delimiters: ['{{', '}}']
}

// ===== 提供全局依赖 =====
app.provide('config', {
  apiUrl: 'https://api.example.com',
  theme: 'dark'
})

// ===== 挂载与卸载 =====
const vm = app.mount('#app') // 返回根组件实例

// 卸载应用
app.unmount()

// ===== 提供注入 =====
app.provide('logger', logger)

// ===== 版本信息 =====
console.log(app.version) // Vue 版本

多应用实例

Vue 3 支持在同一页面创建多个独立的应用实例:

js
import { createApp } from 'vue'
import App1 from './App1.vue'
import App2 from './App2.vue'

// 创建多个独立应用
const app1 = createApp(App1)
app1.mount('#app1')

const app2 = createApp(App2)
app2.mount('#app2')

// 每个应用实例有独立的全局配置
app1.config.globalProperties.$theme = 'dark'
app2.config.globalProperties.$theme = 'light'

// 互不影响

使用场景:

js
// 微前端场景
// 在页面不同区域挂载独立的 Vue 应用
const widgetApp = createApp(WidgetComponent)
widgetApp.mount('#widget-container')

// 主应用
const mainApp = createApp(MainApp)
mainApp.mount('#main-app')

应用配置详解

js
const app = createApp(App)

// config 对象完整结构
app.config = {
  // 全局属性(不推荐使用)
  globalProperties: {
    // 自定义全局属性
  },
  
  // 选项合并策略
  optionMergeStrategies: {
    // 自定义合并策略
    myOption: (toVal, fromVal) => {
      return mergedValue
    }
  },
  
  // 错误处理
  errorHandler: null,
  
  // 警告处理
  warnHandler: null,
  
  // 性能追踪(开发模式)
  performance: false,
  
  // 编译选项
  compilerOptions: {
    // 自定义元素
    isCustomElement: tag => false,
    // 空白处理:'condense' | 'preserve'
    whitespace: 'condense',
    // 分隔符
    delimiters: ['{{', '}}'],
    // 注释
    comments: false
  }
}

组件实例

组件实例结构

js
const instance = {
  // ===== 标识 =====
  uid: 0,                    // 唯一标识符
  type: Component,           // 组件定义对象
  vnode: null,              // 组件 VNode
  subTree: null,            // 渲染的子树 VNode
  
  // ===== 关系 =====
  parent: null,             // 父组件实例
  root: instance,           // 根组件实例
  appContext: null,         // 应用上下文
  
  // ===== 状态 =====
  props: {},                // props 对象
  attrs: {},                // 非 prop 属性
  slots: {},                // 插槽对象
  refs: {},                 // 模板引用
  
  // ===== 响应式状态 =====
  setupState: {},           // setup 返回的状态
  setupState: null,         // setup 返回的响应式状态
  data: null,               // data() 返回的数据
  propsOptions: [],         // props 定义
  emitsOptions: {},         // emits 定义
  
  // ===== 上下文 =====
  ctx: {},                  // 上下文对象
  proxy: null,              // 渲染代理对象
  
  // ===== 渲染相关 =====
  render: null,             // 渲染函数
  renderCache: [],          // 渲染缓存
  update: null,             // 更新函数
  scope: null,              // effect scope
  
  // ===== 生命周期状态 =====
  isMounted: false,         // 是否已挂载
  isUnmounted: false,       // 是否已卸载
  isDeactivated: false,     // 是否已停用(KeepAlive)
  
  // ===== 生命周期钩子 =====
  bc: null,                 // beforeCreate
  c: null,                  // created
  bm: null,                 // beforeMount
  m: null,                  // mounted
  bu: null,                 // beforeUpdate
  u: null,                  // updated
  bum: null,                // beforeUnmount
  um: null,                 // unmounted
  
  // ===== 依赖注入 =====
  provides: {},             // 提供的依赖
  injectOptions: {},        // 注入选项
  
  // ===== 其他 =====
  components: null,         // 局部组件
  directives: null,         // 局部指令
  filters: null,            // 过滤器(Vue 3 已移除)
  inheritAttrs: true,       // 是否继承 attrs
  effects: [],              // 副作用列表
  emits: null,              // 发射事件函数
}

组件实例创建流程

code
┌──────────────────────────────────────────────────────────┐
│                   组件实例创建流程                        │
├──────────────────────────────────────────────────────────┤
│                                                          │
│  1. 创建实例                                             │
│     ┌─────────────────────────────────────────────┐     │
│     │  createComponentInstance()                  │     │
│     │  - 初始化基本属性                            │     │
│     │  - 建立父子关系                              │     │
│     └─────────────────────┬───────────────────────┘     │
│                           │                              │
│                           ▼                              │
│  2. 设置组件                                             │
│     ┌─────────────────────────────────────────────┐     │
│     │  setupComponent()                           │     │
│     │  - 初始化 props                             │     │
│     │  - 初始化 slots                             │     │
│     │  - 执行 setup()                             │     │
│     │  - 处理 Options API                         │     │
│     └─────────────────────┬───────────────────────┘     │
│                           │                              │
│                           ▼                              │
│  3. 设置渲染副作用                                       │
│     ┌─────────────────────────────────────────────┐     │
│     │  setupRenderEffect()                        │     │
│     │  - 创建响应式副作用                          │     │
│     │  - 组件更新时重新渲染                        │     │
│     └─────────────────────────────────────────────┘     │
│                                                          │
└──────────────────────────────────────────────────────────┘

访问组件实例

Composition API

Vue SFC
<script setup>
import { getCurrentInstance, onMounted } from 'vue'

// 获取当前组件实例
// ⚠️ 仅在 setup、生命周期钩子、watch/watchEffect 中使用
const instance = getCurrentInstance()

onMounted(() => {
  // 访问实例属性
  console.log(instance.props)
  console.log(instance.attrs)
  console.log(instance.slots)
  
  // 访问代理对象(类似 Options API 的 this)
  console.log(instance.proxy.count)
  
  // 访问应用上下文
  console.log(instance.appContext.config)
})
</script>

Options API

js
export default {
  data() {
    return { count: 0 }
  },
  methods: {
    increment() {
      // this 是组件代理对象
      this.count++
    }
  },
  mounted() {
    // 访问实例
    console.log(this.$data)
    console.log(this.$props)
    console.log(this.$attrs)
    console.log(this.$el)
    console.log(this.$root)
    console.log(this.$parent)
  }
}

实例属性详解

内置属性

js
// 组件实例暴露的内置属性
export default {
  data() {
    return { count: 0 }
  },
  mounted() {
    // $data - data 对象
    console.log(this.$data)
    
    // $props - props 对象
    console.log(this.$props)
    
    // $attrs - 非 prop 属性
    console.log(this.$attrs)
    
    // $slots - 插槽对象
    console.log(this.$slots)
    
    // $refs - 模板引用
    console.log(this.$refs)
    
    // $el - 根 DOM 元素
    console.log(this.$el)
    
    // $options - 组件选项
    console.log(this.$options)
    
    // $parent - 父组件实例
    console.log(this.$parent)
    
    // $root - 根组件实例
    console.log(this.$root)
    
    // $emit - 发射事件
    this.$emit('update', newValue)
    
    // $forceUpdate - 强制更新
    this.$forceUpdate()
    
    // $nextTick - 下一 tick
    this.$nextTick(() => {
      // DOM 已更新
    })
  }
}

Composition API 对应

Vue SFC
<script setup>
import { 
  useAttrs, 
  useSlots, 
  ref, 
  nextTick,
  getCurrentInstance
} from 'vue'

// 获取 attrs
const attrs = useAttrs()

// 获取 slots
const slots = useSlots()

// 获取 refs
const myRef = ref(null)

// nextTick
nextTick(() => {
  // DOM 已更新
})

// getCurrentInstance
const instance = getCurrentInstance()
</script>

属性访问对比

Options APIComposition API说明
this.$data直接访问 ref/reactive响应式数据
this.$propsdefineProps()组件属性
this.$attrsuseAttrs()非 prop 属性
this.$slotsuseSlots()插槽
this.$refsref() / useTemplateRef()模板引用
this.$emitdefineEmits()发射事件
this.$parentgetCurrentInstance().parent父组件
this.$rootgetCurrentInstance().root根组件
this.$elref() + onMountedDOM 元素
this.$nextTicknextTick()下一 tick
this.$forceUpdate-强制更新(避免使用)

与 Vue 2 的区别

应用实例 vs 全局 API

js
// ===== Vue 2 =====
import Vue from 'vue'

// 全局注册组件
Vue.component('MyComponent', MyComponent)

// 全局注册指令
Vue.directive('focus', FocusDirective)

// 全局配置
Vue.prototype.$http = axios

// 创建实例
new Vue({
  el: '#app',
  router,
  store
})

// ===== Vue 3 =====
import { createApp } from 'vue'

const app = createApp(App)

// 应用级别注册组件
app.component('MyComponent', MyComponent)

// 应用级别注册指令
app.directive('focus', FocusDirective)

// 应用级别配置
app.config.globalProperties.$http = axios
// 或使用 provide/inject
app.provide('http', axios)

// 挂载
app.use(router).use(store).mount('#app')

差异对比表

特性Vue 2Vue 3
创建应用new Vue()createApp()
全局 APIVue.xxxapp.xxx
全局属性Vue.prototype.xxxapp.config.globalProperties.xxx
多应用不支持支持
this 指向组件实例组件代理对象
mixin全局/组件级别仅组件级别
过滤器支持移除(使用方法调用)
$on/$off/$once支持移除(使用外部库)
$children支持移除(使用 ref)
$scopedSlots支持合并为 $slots

this 指向变化

js
// Vue 2
export default {
  data() {
    return { count: 0 }
  },
  mounted() {
    // this 直接是组件实例
    console.log(this._uid)      // 实例 ID
    console.log(this._data)     // data 对象
    console.log(this.$data)     // 同上
  }
}

// Vue 3
export default {
  data() {
    return { count: 0 }
  },
  mounted() {
    // this 是组件代理对象(proxy)
    // 不能访问实例内部属性(如 _uid)
    
    // 但可以访问:
    console.log(this.$data)     // data 对象
    console.log(this.count)     // 数据属性
    console.log(this.myMethod)  // 方法
  }
}

常见问题

Q1: 为什么 Vue 3 将全局 API 改为应用实例?

A: Vue 2 的全局 API 会影响所有 Vue 实例,导致:

  • 第三方库可能污染全局
  • 多个 Vue 实例之间相互影响
  • 测试困难

Vue 3 的应用实例隔离:

  • 每个应用有独立的配置
  • 支持多应用实例
  • 更好的 Tree-shaking
js
// Vue 2 问题
Vue.component('Button', ComponentA)
// 所有 Vue 实例都会注册这个组件

// Vue 3 解决
const app1 = createApp(App1)
app1.component('Button', ComponentA) // 只在 app1 中可用

const app2 = createApp(App2)
app2.component('Button', ComponentB) // app2 有自己的 Button

Q2: 如何在 Composition API 中访问组件实例?

A: 使用 getCurrentInstance(),但应避免滥用。

Vue SFC
<script setup>
import { getCurrentInstance } from 'vue'

const instance = getCurrentInstance()

// ⚠️ 仅在以下时机使用:
// - setup() 中
// - 生命周期钩子中
// - watch/watchEffect 中

// ❌ 不要在异步回调中使用
setTimeout(() => {
  const instance = getCurrentInstance() // 可能是 null
}, 1000)

// ✅ 在 setup 中保存引用
const { proxy } = getCurrentInstance()
setTimeout(() => {
  proxy.$emit('event') // 使用保存的引用
}, 1000)
</script>

Q3: 如何访问 $attrs 和 $listeners?

A: Vue 3 移除了 $listeners,合并到 $attrs。

js
// Vue 2
this.$attrs      // 非 prop 属性
this.$listeners  // 事件监听器

// Vue 3
import { useAttrs } from 'vue'

const attrs = useAttrs()
// attrs 包含属性和事件监听器
// 如 { class: 'btn', onClick: fn }

Q4: 如何实现全局属性?

A: 推荐使用 provide/inject 而非 globalProperties。

js
// 方式1: globalProperties(不推荐)
app.config.globalProperties.$http = axios

// 使用
export default {
  mounted() {
    this.$http.get('/api')
  }
}

// 方式2: provide/inject(推荐)
// main.js
app.provide('http', axios)

// 组件中
import { inject } from 'vue'
const http = inject('http')

// 方式3: 组合式函数(最佳)
// useHttp.js
import axios from 'axios'
export function useHttp() {
  return axios
}

Q5: $forceUpdate 还需要吗?

A: 通常不需要。Vue 3 的响应式系统更完善,应该确保数据是响应式的。

js
// ❌ 错误做法
data() {
  return {
    obj: { a: 1 } // Vue 2 中添加属性不响应
  }
},
methods: {
  addProp() {
    this.obj.b = 2
    this.$forceUpdate() // 强制更新
  }
}

// ✅ 正确做法(Vue 3)
import { reactive } from 'vue'

const obj = reactive({ a: 1 })
obj.b = 2 // 自动响应,无需强制更新

最佳实践

组件实例设计

js
// ✅ 推荐:使用 Composition API
<script setup>
import { ref, computed, provide, inject } from 'vue'

// 响应式状态
const count = ref(0)

// 计算属性
const doubled = computed(() => count.value * 2)

// 依赖注入
const config = inject('config')

// 提供
provide('theme', 'dark')
</script>

// ✅ 推荐:组合式函数
<script setup>
import { useCounter } from './composables/useCounter'

const { count, increment } = useCounter()
</script>

应用实例配置

js
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import { createPinia } from 'pinia'

const app = createApp(App)

// 插件
app.use(router)
app.use(createPinia())

// 全局错误处理
app.config.errorHandler = (err, instance, info) => {
  // 上报错误
  reportError(err, info)
}

// 性能追踪(开发环境)
if (import.meta.env.DEV) {
  app.config.performance = true
}

// 挂载
app.mount('#app')

下一步


生命周期详解

Vue 3 组件从创建到销毁经历完整的生命周期。Composition API 以 on 前缀的函数形式提供生命周期钩子,setup() 替代了 beforeCreatecreated

理解 Vue 组件的完整生命周期,包括各阶段的特点和使用场景,是编写高质量组件的关键。

生命周期概览

完整流程图

code
┌────────────────────────────────────────────────────────────┐
│                    Vue 3 组件生命周期                       │
├────────────────────────────────────────────────────────────┤
│                                                            │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                    创建阶段                          │  │
│  │                                                     │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ setup() - Composition API 入口              │   │  │
│  │  │ - 解析 props                                │   │  │
│  │  │ - 创建响应式状态                             │   │  │
│  │  │ - 返回模板使用的数据和方法                   │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  │                       │                             │  │
│  │                       ▼                             │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ beforeCreate - Options API                  │   │  │
│  │  │ - data/methods 未初始化                      │   │  │
│  │  │ - 无法访问 this                              │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  │                       │                             │  │
│  │                       ▼                             │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ created - Options API                        │   │  │
│  │  │ - data/methods 已初始化                      │   │  │
│  │  │ - 可访问数据和方法                           │   │  │
│  │  │ - DOM 尚未挂载                               │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
│                            │                               │
│                            ▼                               │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                    挂载阶段                          │  │
│  │                                                     │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ beforeMount                                  │   │  │
│  │  │ - 模板已编译为渲染函数                       │   │  │
│  │  │ - 即将创建 DOM                               │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  │                       │                             │  │
│  │                       ▼                             │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ mounted                                      │   │  │
│  │  │ - DOM 已创建并挂载                           │   │  │
│  │  │ - 可访问 DOM 元素                            │   │  │
│  │  │ - 子组件可能未完成挂载                       │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
│                            │                               │
│                            ▼                               │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                    更新阶段                          │  │
│  │                                                     │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ beforeUpdate                                 │   │  │
│  │  │ - 数据已更新                                 │   │  │
│  │  │ - DOM 尚未更新                               │   │  │
│  │  │ - 可获取更新前的 DOM 状态                    │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  │                       │                             │  │
│  │                       ▼                             │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ updated                                      │   │  │
│  │  │ - DOM 已更新                                 │   │  │
│  │  │ - 避免在此修改状态(可能无限循环)           │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
│                            │                               │
│                            ▼                               │
│  ┌─────────────────────────────────────────────────────┐  │
│  │                    卸载阶段                          │  │
│  │                                                     │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ beforeUnmount                                │   │  │
│  │  │ - 组件即将卸载                               │   │  │
│  │  │ - 功能仍然正常                               │   │  │
│  │  │ - 清理前的准备工作                           │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  │                       │                             │  │
│  │                       ▼                             │  │
│  │  ┌─────────────────────────────────────────────┐   │  │
│  │  │ unmounted                                    │   │  │
│  │  │ - 组件已卸载                                 │   │  │
│  │  │ - 清理所有副作用                             │   │  │
│  │  │ - 移除事件监听、定时器等                     │   │  │
│  │  └─────────────────────────────────────────────┘   │  │
│  └─────────────────────────────────────────────────────┘  │
│                                                            │
└────────────────────────────────────────────────────────────┘

钩子对比表

Composition APIOptions API执行时机
setup()-最先执行
-beforeCreatesetup 之后
-created响应式数据创建后
onBeforeMountbeforeMountDOM 挂载前
onMountedmountedDOM 挂载后
onBeforeUpdatebeforeUpdate数据更新,DOM 更新前
onUpdatedupdatedDOM 更新后
onBeforeUnmountbeforeUnmount卸载前
onUnmountedunmounted卸载后

各阶段详解

创建阶段

setup()

Vue SFC
<script setup>
import { ref, onMounted } from 'vue'

// setup 在组件创建前执行
// 此时 props 已解析,但组件实例尚未完全创建

const props = defineProps({
  title: String
})

const count = ref(0)

// 无法访问组件实例
// const instance = getCurrentInstance() // ✅ 可以获取
// console.log(instance.proxy) // ❌ 此时 proxy 尚未创建

onMounted(() => {
  console.log('mounted')
})
</script>

beforeCreate / created

js
export default {
  data() {
    return { count: 0 }
  },
  
  beforeCreate() {
    // data、methods、computed 等尚未初始化
    console.log(this.count) // undefined
    console.log(this.increment) // undefined
  },
  
  created() {
    // data、methods、computed 已初始化
    console.log(this.count) // 0
    this.increment() // ✅ 可调用
    
    // 但 DOM 尚未挂载
    console.log(this.$el) // null
    
    // 适合:初始化数据、发起网络请求
    this.fetchData()
  },
  
  methods: {
    increment() {
      this.count++
    }
  }
}

挂载阶段

Vue SFC
<template>
  <div ref="container">
    <h1>{{ title }}</h1>
  </div>
</template>

<script setup>
import { ref, onBeforeMount, onMounted } from 'vue'

const container = ref(null)
const title = ref('Hello')

onBeforeMount(() => {
  // 模板已编译,但 DOM 尚未创建
  console.log(container.value) // null
  console.log('DOM 即将挂载')
})

onMounted(() => {
  // DOM 已挂载,可以安全访问
  console.log(container.value) // <div>...</div>
  console.log(container.value.getBoundingClientRect())
  
  // 初始化第三方库
  const chart = new Chart(container.value, config)
  
  // ⚠️ 注意:子组件可能尚未完成挂载
  // 如需确保子组件已挂载,使用 nextTick
})
</script>

更新阶段

Vue SFC
<template>
  <div>
    <p ref="paragraph">{{ count }}</p>
    <button @click="count++">Increment</button>
  </div>
</template>

<script setup>
import { ref, onBeforeUpdate, onUpdated, nextTick } from 'vue'

const count = ref(0)
const paragraph = ref(null)

onBeforeUpdate(() => {
  // 数据已更新,DOM 尚未更新
  console.log('DOM 中的值:', paragraph.value.textContent)
  console.log('新值:', count.value)
})

onUpdated(() => {
  // DOM 已更新
  console.log('DOM 中的值:', paragraph.value.textContent)
  
  // ⚠️ 避免在此修改状态,可能导致无限循环
  // count.value++ // ❌ 危险!
  
  // 如需基于更新后的 DOM 执行操作
  nextTick(() => {
    // 安全操作
  })
})
</script>

卸载阶段

Vue SFC
<script setup>
import { ref, onBeforeUnmount, onUnmounted } from 'vue'

let timer
let resizeObserver

onMounted(() => {
  // 设置定时器
  timer = setInterval(() => {
    console.log('tick')
  }, 1000)
  
  // 设置观察器
  resizeObserver = new ResizeObserver(() => {})
  resizeObserver.observe(document.body)
  
  // 添加事件监听
  window.addEventListener('resize', handleResize)
})

onBeforeUnmount(() => {
  // 组件即将卸载,功能仍正常
  console.log('准备清理')
})

onUnmounted(() => {
  // 清理所有副作用
  clearInterval(timer)
  resizeObserver.disconnect()
  window.removeEventListener('resize', handleResize)
  
  // 取消网络请求
  controller.abort()
})
</script>

生命周期钩子详解

Composition API 钩子

Vue SFC
<script setup>
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onRenderTracked,
  onRenderTriggered
} from 'vue'

// 所有钩子都支持注册多个回调
onMounted(() => {
  console.log('mounted 1')
})

onMounted(() => {
  console.log('mounted 2')
})

// 钩子返回清理函数(部分钩子支持)
onMounted(() => {
  const timer = setInterval(() => {}, 1000)
  // 返回清理函数(Vue 3.5+)
  return () => clearInterval(timer)
})

// 调试钩子
onRenderTracked((e) => {
  console.log('render tracked:', e)
})

onRenderTriggered((e) => {
  console.log('render triggered:', e)
})
</script>

Options API 钩子

js
export default {
  beforeCreate() {
    console.log('beforeCreate')
  },
  
  created() {
    console.log('created')
  },
  
  beforeMount() {
    console.log('beforeMount')
  },
  
  mounted() {
    console.log('mounted')
    // 可以访问 this
    console.log(this.$el)
  },
  
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  
  updated() {
    console.log('updated')
  },
  
  beforeUnmount() {
    console.log('beforeUnmount')
  },
  
  unmounted() {
    console.log('unmounted')
  }
}

钩子执行顺序

code
父子组件挂载顺序:
1. 父 beforeCreate
2. 父 created
3. 父 beforeMount
4. 子 beforeCreate
5. 子 created
6. 子 beforeMount
7. 子 mounted
8. 父 mounted

父子组件更新顺序:
1. 父 beforeUpdate
2. 子 beforeUpdate
3. 子 updated
4. 父 updated

父子组件卸载顺序:
1. 父 beforeUnmount
2. 子 beforeUnmount
3. 子 unmounted
4. 父 unmounted

KeepAlive 相关钩子

onActivated / onDeactivated

Vue SFC
<template>
  <KeepAlive>
    <MyComponent v-if="show" />
  </KeepAlive>
</template>

<!-- MyComponent.vue -->
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 组件从缓存中恢复
  console.log('activated - 组件激活')
  
  // 重新获取数据
  fetchData()
  
  // 恢复滚动位置
  restoreScrollPosition()
})

onDeactivated(() => {
  // 组件进入缓存
  console.log('deactivated - 组件停用')
  
  // 保存状态
  saveScrollPosition()
  
  // 暂停不必要的操作
  pauseVideo()
})
</script>

KeepAlive 完整示例

Vue SFC
<template>
  <div>
    <button @click="current = 'A'">Tab A</button>
    <button @click="current = 'B'">Tab B</button>
    
    <KeepAlive>
      <component :is="current === 'A' ? TabA : TabB" />
    </KeepAlive>
  </div>
</template>

<script setup>
import { ref } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'

const current = ref('A')
</script>

<!-- TabA.vue -->
<script setup>
import { onMounted, onUnmounted, onActivated, onDeactivated } from 'vue'

onMounted(() => {
  console.log('TabA mounted - 首次加载')
})

onUnmounted(() => {
  console.log('TabA unmounted - 被 KeepAlive 时不会触发')
})

onActivated(() => {
  console.log('TabA activated - 切换到此 Tab')
})

onDeactivated(() => {
  console.log('TabA deactivated - 切换到其他 Tab')
})
</script>

错误处理钩子

onErrorCaptured

Vue SFC
<script setup>
import { onErrorCaptured, ref } from 'vue'

const error = ref(null)

onErrorCaptured((err, instance, info) => {
  // err: 错误对象
  // instance: 发生错误的组件实例
  // info: 错误来源信息
  
  console.error('Captured error:', err)
  console.log('Component:', instance)
  console.log('Error info:', info)
  
  // 存储错误用于显示
  error.value = err.message
  
  // 返回 false 阻止错误继续向上传播
  return false
  
  // 返回 true 或不返回,错误继续传播
})
</script>

<template>
  <div v-if="error" class="error">
    {{ error }}
  </div>
  <slot v-else />
</template>

错误边界组件

Vue SFC
<!-- ErrorBoundary.vue -->
<script setup>
import { ref, onErrorCaptured } from 'vue'

const error = ref(null)
const errorInfo = ref(null)

onErrorCaptured((err, instance, info) => {
  error.value = err
  errorInfo.value = info
  
  // 阻止错误传播
  return false
})

const resetError = () => {
  error.value = null
  errorInfo.value = null
}
</script>

<template>
  <slot v-if="!error" />
  <div v-else class="error-boundary">
    <h2>Something went wrong</h2>
    <p>{{ error.message }}</p>
    <button @click="resetError">Try again</button>
  </div>
</template>

服务端渲染钩子

onServerPrefetch

Vue SFC
<script setup>
import { ref, onServerPrefetch } from 'vue'
import { fetchData } from './api'

const data = ref(null)

// 仅在服务端执行
onServerPrefetch(async () => {
  // 在服务端预取数据
  data.value = await fetchData()
  
  // 组件会等待此 Promise 完成
})
</script>

使用场景最佳实践

数据请求

Vue SFC
<script setup>
import { ref, onMounted, onServerPrefetch } from 'vue'

const data = ref(null)
const loading = ref(true)
const error = ref(null)

// 服务端预取
onServerPrefetch(async () => {
  try {
    data.value = await fetchData()
  } catch (e) {
    error.value = e
  }
})

// 客户端请求
onMounted(async () => {
  // 服务端已预取则跳过
  if (data.value) return
  
  try {
    data.value = await fetchData()
  } catch (e) {
    error.value = e
  } finally {
    loading.value = false
  }
})
</script>

DOM 操作

Vue SFC
<script setup>
import { ref, onMounted, onUpdated, nextTick } from 'vue'

const container = ref(null)
const items = ref([])

onMounted(() => {
  // DOM 已挂载
  initPlugin()
})

onUpdated(() => {
  // DOM 已更新
  nextTick(() => {
    // 更新插件
    updatePlugin()
  })
})

const initPlugin = () => {
  const el = container.value
  // 初始化第三方插件
}

const updatePlugin = () => {
  // 更新插件状态
}
</script>

<template>
  <div ref="container">
    <div v-for="item in items" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

清理副作用

Vue SFC
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 定时器
let timer = null
const count = ref(0)

onMounted(() => {
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

onUnmounted(() => {
  clearInterval(timer)
})
</script>

<script setup>
// 更优雅的方式:使用 watchEffect 自动清理
import { watchEffect } from 'vue'

const count = ref(0)

watchEffect((onCleanup) => {
  const timer = setInterval(() => {
    count.value++
  }, 1000)
  
  onCleanup(() => {
    clearInterval(timer)
  })
})
</script>

<script setup>
// 使用 VueUse 的 useInterval
import { useInterval } from '@vueuse/core'

const count = useInterval(1000)
</script>

事件监听

Vue SFC
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const scrollY = ref(0)

const handleScroll = () => {
  scrollY.value = window.scrollY
}

onMounted(() => {
  window.addEventListener('scroll', handleScroll)
})

onUnmounted(() => {
  window.removeEventListener('scroll', handleScroll)
})
</script>

<script setup>
// 使用组合式函数封装
import { useEventListener } from '@vueuse/core'

const scrollY = ref(0)

useEventListener(window, 'scroll', () => {
  scrollY.value = window.scrollY
})
// 自动清理
</script>

调试技巧

使用生命周期钩子调试

Vue SFC
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'

// 开发环境调试
if (import.meta.env.DEV) {
  const name = 'MyComponent'
  
  onMounted(() => {
    console.log(`[${name}] mounted`)
  })
  
  onUpdated(() => {
    console.log(`[${name}] updated`)
  })
  
  onUnmounted(() => {
    console.log(`[${name}] unmounted`)
  })
}
</script>

使用 Vue DevTools

code
1. 安装 Vue DevTools 浏览器扩展
2. 打开开发者工具,切换到 Vue 标签
3. 在 Timeline 中查看:
   - 组件挂载/更新/卸载
   - 性能追踪
   - 事件触发

onRenderTracked / onRenderTriggered

Vue SFC
<script setup>
import { ref, onRenderTracked, onRenderTriggered } from 'vue'

const count = ref(0)

// 追踪响应式依赖
onRenderTracked((e) => {
  console.log('tracked:', e.key, e.target)
})

// 触发更新的依赖
onRenderTriggered((e) => {
  console.log('triggered:', e.key, e.newValue, e.oldValue)
})
</script>

与 Vue 2 的区别

命名变化

Vue 2Vue 3说明
beforeDestroybeforeUnmount卸载前
destroyedunmounted卸载后

移除的钩子

js
// Vue 2 有,Vue 3 移除
export default {
  // ❌ 不再存在
  beforeRouteEnter() {},
  beforeRouteUpdate() {},
  beforeRouteLeave() {}
}

// ✅ 使用 Composition API 替代
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave((to, from, next) => {
  // ...
})

选项式 API vs 组合式 API

js
// Options API
export default {
  data() {
    return { count: 0 }
  },
  mounted() {
    console.log('mounted')
  },
  updated() {
    console.log('updated')
  }
}

// Composition API - 更灵活
import { ref, onMounted, onUpdated } from 'vue'

const count = ref(0)

onMounted(() => {
  console.log('mounted')
})

// 可以注册多个钩子
onMounted(() => {
  console.log('mounted again')
})

onUpdated(() => {
  console.log('updated')
})

常见问题

Q1: setup 和 created 有什么区别?

A: setupbeforeCreate 之前执行,是 Composition API 的入口。

js
// 执行顺序
// 1. setup()
// 2. beforeCreate
// 3. created

// setup 中
export default {
  setup() {
    // 没有 this
    console.log(this) // undefined
  },
  created() {
    // 有 this
    console.log(this) // 组件代理
  }
}

Q2: 为什么在 mounted 中访问子组件需要 nextTick?

A: 父组件 mounted 时,子组件可能尚未完成挂载。

Vue SFC
<script setup>
import { ref, onMounted, nextTick } from 'vue'

const childRef = ref(null)

onMounted(() => {
  console.log(childRef.value) // 可能为 null
  
  nextTick(() => {
    console.log(childRef.value) // 确保子组件已挂载
  })
})
</script>

<template>
  <ChildComponent ref="childRef" />
</template>

Q3: 如何在异步回调中使用生命周期钩子?

A: 钩子必须在同步的 setup 中注册。

Vue SFC
<script setup>
import { onMounted } from 'vue'

// ❌ 错误:异步注册
fetchData().then(() => {
  onMounted(() => {}) // 警告:不在组件上下文中
})

// ✅ 正确:同步注册,异步执行
onMounted(async () => {
  await fetchData()
})
</script>

Q4: onUpdated 中修改状态会导致无限循环吗?

A: 是的,需要谨慎使用。

Vue SFC
<script setup>
import { ref, onUpdated } from 'vue'

const count = ref(0)

// ❌ 无限循环
onUpdated(() => {
  count.value++
})

// ✅ 条件更新
onUpdated(() => {
  if (count.value < 10) {
    count.value++
  }
})

// ✅ 使用 nextTick
onUpdated(() => {
  nextTick(() => {
    // 在下一个 tick 执行,避免循环
  })
})
</script>

Q5: KeepAlive 组件的生命周期如何触发?

A: 首次加载走完整流程,后续切换只触发 activated/deactivated。

code
首次加载:
mounted → activated

切换走:
deactivated

切换回:
activated

真正卸载:
deactivated → unmounted

最佳实践总结

  1. 使用 Composition API:更灵活、更易复用
  2. 及时清理副作用:在 onUnmounted 中清理
  3. 避免在 updated 中修改状态:可能导致无限循环
  4. 使用 nextTick:确保 DOM 更新完成
  5. 封装组合式函数:将生命周期逻辑封装
  6. 使用 DevTools 调试:查看组件状态和性能
Vue SFC
<script setup>
// 封装生命周期逻辑
function useAsyncData(fetchFn) {
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      data.value = await fetchFn()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  onMounted(execute)
  
  return { data, loading, error, execute }
}

// 使用
const { data, loading, error } = useAsyncData(() => fetchUser())
</script>

下一步