Vue 3 组件实例完全指南
本文档从零开始讲解 Vue 3 的组件实例:什么是组件实例、何时创建、实例上有什么、在选项式 API 与组合式 API(含 )里如何访问实例、如何通过 defineExpose 暴露给父组件,以及常见用法示例,适合新手系统学习。
一、什么是“组件实例”?
1.1 生活中的类比
一个.vue 文件好比“设计图”,组件实例则是根据这张设计图真正造出来的那一份。
你在模板里写一次 ,Vue 就会为它创建一个组件实例:拥有自己的 data、props、DOM,和别的 互不干扰。
所以:一个组件可以对应多个实例(同一份“设计图”,多份“实体”)。
1.2 实例里有什么?
组件实例是 Vue 在内部维护的一个对象,上面挂载了:
- 你定义的 data、computed、methods(在选项式里),或 setup 返回的内容;
- Vue 提供的内置属性/方法:$data、$props、$el、$refs、$parent、$root、$attrs、$slots、$emit、$nextTick 等。
在选项式 API 里,我们通过 this 访问这个实例;在 里没有 this,但可以通过 getCurrentInstance() 或父组件对子组件的 ref 间接和“实例”打交道。
二、实例何时被创建?
2.1 从“创建”到“挂载”
组件实例在创建阶段就生成了(在 beforeCreate 之前就开始准备,setup 或 created 时已经可用)。
此时已有 props、data(或 setup 的返回值),但还没有 DOM($el 还是 undefined)。
等到挂载阶段结束(mounted 之后),实例才会拥有 $el(根 DOM),$refs 也会被填上。
简单记:
- 创建后:有实例、有数据(data/setup 返回值),没有 $el、$refs。
- 挂载后:有 $el、$refs,可以安全操作 DOM。
2.2 每个“使用”对应一个实例
模板里每写一次组件标签,就会对应一个组件实例:
<UserCard />
<UserCard />
这里会有两个 UserCard 的实例,各自有各自的 data、DOM,互不影响。
三、在选项式 API 里访问实例:this
3.1 this 就是当前实例
在 data、computed、methods、watch、生命周期钩子里,this 指向当前组件实例。
所以 this.xxx 可以访问 data、computed、methods,以及 this.$refs、this.$emit 等内置属性/方法。
export default {
data() {
return { count: 0 }
},
methods: {
add() {
this.count++
console.log(this.$el)
}
},
mounted() {
console.log(this)
}
}
3.2 注意 this 的指向
在回调里(如 setTimeout、axios.then),要确保 this 仍是组件实例;若用了普通函数,可能变成 undefined 或其它对象。
此时可事先存 const that = this,或在回调里用箭头函数(箭头函数不绑自己的 this,会沿用外层的 this)。
methods: {
fetch() {
const that = this
fetch('/api').then(function (res) {
that.list = res.data
})
},
fetch2() {
fetch('/api').then((res) => {
this.list = res.data
})
}
}
四、实例上常见内容一览
(以下在挂载完成后的上下文中讨论;未挂载时 $el、$refs 可能尚未就绪。)
| 属性/方法 | 含义 | 何时可用 |
|---|---|---|
| this.xxx | data、computed、methods、setup 返回值 | 创建后 |
| $data | data 返回的对象 | 创建后 |
| $props | 当前 props 对象 | 创建后 |
| $el | 根 DOM 元素 | 挂载后 |
| $refs | 模板 ref 集合 | 挂载后 |
| $parent | 父组件实例 | 挂载后 |
| $root | 根组件实例 | 挂载后 |
| $attrs | 未声明的属性/事件 | 创建后 |
| $slots | 插槽内容 | 创建后 |
| $emit | 触发自定义事件 | 创建后 |
| $nextTick | DOM 更新后执行回调 | 创建后 |
更详细的说明见《Vue3内置属性.md》。
五、在组合式 API / 里“没有 this”
5.1 为什么没有 this?
在 setup() 或 里,代码执行时还没有把数据挂到实例上,所以不提供 this。
数据和方法都是你自己定义的变量和函数,通过 return(setup 函数)或顶层暴露()给模板用,而不是通过 this.xxx。
5.2 需要“当前实例”时:getCurrentInstance()
若在 setup 或 里需要拿到当前组件实例(例如取 $el、$parent),可以用 getCurrentInstance()。
它返回一个内部实例对象,其中 proxy 相当于选项式里的 this(仅建议在库或高级场景使用,业务代码优先用 ref、provide/inject 等)。
import { getCurrentInstance } from 'vue'
const instance = getCurrentInstance()
console.log(instance?.proxy?.$el)
console.log(instance?.proxy?.$refs)
console.log(instance?.parent)
注意:getCurrentInstance() 只在 setup 同步执行期间(或同步生命周期钩子)有效,在异步回调里可能为 null,不要依赖在 setTimeout、Promise.then 里再调一次 getCurrentInstance()。
六、父组件访问子组件实例:ref + defineExpose
6.1 父组件用 ref 拿到子组件
在父组件里,给子组件标签加上 ref,并在 script 里声明一个 ref(null) 与之同名;挂载后,xxxRef.value 默认是子组件的实例 proxy(即“组件实例”对外暴露的那一层)。
在 里,子组件默认不暴露任何东西,所以 xxxRef.value 可能是 null 或一个空对象,除非子组件用 defineExpose 显式暴露。
6.2 子组件用 defineExpose 暴露给父组件
在 里,defineExpose 用来声明:父组件通过 ref 拿到子组件时,能访问到哪些属性和方法。
传入一个对象,键是暴露出去的名字,值是对应的变量或函数。
子组件:
<!-- Child.vue -->
<template>
<div>
<p>{{ count }}</p>
<button @click="add">+1</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
function add() {
count.value++
}
function getCount() {
return count.value
}
defineExpose({
count,
add,
getCount
})
</script>
父组件:
<template>
<Child ref="childRef" />
<button @click="callChild">调用子组件的 add</button>
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
function callChild() {
childRef.value?.add()
console.log(childRef.value?.getCount())
}
</script>
- childRef.value 就是子组件 defineExpose 传出的对象(这里包含 count、add、getCount)。
- 若子组件没有 defineExpose,在 下父组件拿到的 childRef.value 里就没有这些属性。
6.3 选项式子组件不用 defineExpose
选项式 API 的子组件,没有 defineExpose;父组件用 ref 拿到子组件时,ref.value 就是子组件的实例 proxy,可以直接访问子组件的 data、methods 等(相当于选项式里的 this 上的内容)。
七、实例与生命周期
7.1 创建阶段
- beforeCreate 之前:实例正在创建,data、methods 等还未初始化。
- created(或 setup 执行完):实例已创建好,data、computed、methods(或 setup 返回值)可用,$el、$refs 仍不可用。
7.2 挂载阶段
- mounted 之后:$el、$refs 可用,可以安全操作 DOM、调用子组件通过 ref 暴露的方法。
7.3 卸载阶段
- unmounted 之后:实例被销毁,$el 被移除,不应再访问实例上的 DOM 或依赖该实例的回调。
八、完整示例:父调子方法、子调父方法
8.1 父通过 ref 调子(defineExpose)
见上文“父组件访问子组件实例”中的 Child.vue 与父组件示例。
8.2 子通过 emit 通知父
子组件不暴露实例也可以;通过 emit 通知父组件,父组件在 @事件名 里处理:
<!-- 子 -->
<button @click="$emit('submit', form)">提交</button>
<!-- 父 -->
<Child @submit="onSubmit" />
function onSubmit(form) {
console.log(form)
}
“父调子”用 ref + defineExpose,“子调父”用 emit,是 Vue 推荐的组件通信方式。
九、常见问题与注意点
9.1 挂载前不要访问 $el、$refs
在 created 或 setup 里 this.$el、this.$refs.xxx(或 inputRef.value)可能为 undefined,应把依赖 DOM 的逻辑放到 mounted(或 onMounted)里,必要时配合 nextTick。
9.2 下父拿子必须 defineExpose
子组件用 时,父组件 ref 得到的是 defineExpose 传出的对象;若子组件没写 defineExpose,父组件就拿不到子组件内部的数据和方法。
9.3 getCurrentInstance 不要在异步里依赖
getCurrentInstance() 只在当前 setup 同步执行栈内有效,异步回调里再调用可能得到 null;需要实例上的信息时,在 setup 里先取出来存到变量里再在异步里用。
9.4 不要依赖 $parent、$root 做主要通信
$parent、$root 会令组件和“谁用我”强耦合,不利于复用;优先用 props、emit、provide/inject 或 Pinia。
十、组件实例速查表
| 内容 | 选项式 API | |
|---|---|---|
| 访问当前实例 | this | 无 this;用 getCurrentInstance()?.proxy 仅高级场景 |
| 数据/方法 | this.xxx | 直接变量名、函数名 |
| 父访问子 | 父 ref → this.$refs.xxx 即子实例 | 子 defineExpose;父 ref → xxxRef.value 为暴露对象 |
| 实例创建完成 | created 之后 | setup 执行完 |
| $el、$refs 可用 | mounted 之后 | onMounted 里用 ref 等 |
十一、学习建议
- 先理解:一个组件标签 = 一个组件实例,实例上有 data、methods 和 $ 系列属性。
- 在选项式里熟练用 this 访问数据和 $refs、$emit、$nextTick;在 里用变量和 defineEmits、ref 等,不依赖 this。
- 父要调子:子用 defineExpose 暴露方法/数据,父用 ref 拿到并调用;子要调父:用 emit。
- 需要“当前实例”时再查 getCurrentInstance(),并注意其使用限制。
把本文档里的“父 ref + 子 defineExpose”和“子 emit”示例在项目里各做一遍,会掌握得更牢。更多内置属性见《Vue3内置属性.md》。祝你学习顺利。