应用实例与组件实例
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 API | Composition API | 说明 |
|---|---|---|
this.$data | 直接访问 ref/reactive | 响应式数据 |
this.$props | defineProps() | 组件属性 |
this.$attrs | useAttrs() | 非 prop 属性 |
this.$slots | useSlots() | 插槽 |
this.$refs | ref() / useTemplateRef() | 模板引用 |
this.$emit | defineEmits() | 发射事件 |
this.$parent | getCurrentInstance().parent | 父组件 |
this.$root | getCurrentInstance().root | 根组件 |
this.$el | ref() + onMounted | DOM 元素 |
this.$nextTick | nextTick() | 下一 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 2 | Vue 3 |
|---|---|---|
| 创建应用 | new Vue() | createApp() |
| 全局 API | Vue.xxx | app.xxx |
| 全局属性 | Vue.prototype.xxx | app.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 有自己的 ButtonQ2: 如何在 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()替代了beforeCreate和created。
理解 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 API | Options API | 执行时机 |
|---|---|---|
setup() | - | 最先执行 |
| - | beforeCreate | setup 之后 |
| - | created | 响应式数据创建后 |
onBeforeMount | beforeMount | DOM 挂载前 |
onMounted | mounted | DOM 挂载后 |
onBeforeUpdate | beforeUpdate | 数据更新,DOM 更新前 |
onUpdated | updated | DOM 更新后 |
onBeforeUnmount | beforeUnmount | 卸载前 |
onUnmounted | unmounted | 卸载后 |
各阶段详解
创建阶段
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. 父 unmountedKeepAlive 相关钩子
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 2 | Vue 3 | 说明 |
|---|---|---|
beforeDestroy | beforeUnmount | 卸载前 |
destroyed | unmounted | 卸载后 |
移除的钩子
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: setup 在 beforeCreate 之前执行,是 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最佳实践总结
- 使用 Composition API:更灵活、更易复用
- 及时清理副作用:在
onUnmounted中清理 - 避免在
updated中修改状态:可能导致无限循环 - 使用
nextTick:确保 DOM 更新完成 - 封装组合式函数:将生命周期逻辑封装
- 使用 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>下一步
- 单文件组件 - 学习 Vue 组件的完整开发流程