Vue3组件实例

Vue 3 组件实例完全指南

本文档从零开始讲解 Vue 3 的组件实例:什么是组件实例、何时创建、实例上有什么、在选项式 API 与组合式 API(含 )里如何访问实例、如何通过 defineExpose 暴露给父组件,以及常见用法示例,适合新手系统学习。


一、什么是“组件实例”?

1.1 生活中的类比

一个.vue 文件好比“设计图”,组件实例则是根据这张设计图真正造出来的那一份
你在模板里写一次 ,Vue 就会为它创建一个组件实例:拥有自己的 datapropsDOM,和别的 互不干扰。
所以:一个组件可以对应多个实例(同一份“设计图”,多份“实体”)。

1.2 实例里有什么?

组件实例是 Vue 在内部维护的一个对象,上面挂载了:

  • 你定义的 datacomputedmethods(在选项式里),或 setup 返回的内容;
  • Vue 提供的内置属性/方法:$data$props$el$refs$parent$root$attrs$slots$emit$nextTick 等。

选项式 API 里,我们通过 this 访问这个实例;在 里没有 this,但可以通过 getCurrentInstance()父组件对子组件的 ref 间接和“实例”打交道。


二、实例何时被创建?

2.1 从“创建”到“挂载”

组件实例在创建阶段就生成了(在 beforeCreate 之前就开始准备,setupcreated 时已经可用)。
此时已有 propsdata(或 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 就是当前实例

datacomputedmethodswatch生命周期钩子里,this 指向当前组件实例
所以 this.xxx 可以访问 datacomputedmethods,以及 this.$refsthis.$emit 等内置属性/方法。

export default {
  data() {
    return { count: 0 }
  },
  methods: {
    add() {
      this.count++
      console.log(this.$el)
    }
  },
  mounted() {
    console.log(this)
  }
}

3.2 注意 this 的指向

回调里(如 setTimeoutaxios.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(仅建议在库或高级场景使用,业务代码优先用 refprovide/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,不要依赖在 setTimeoutPromise.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 传出的对象(这里包含 countaddgetCount)。
  • 若子组件没有 defineExpose,在 下父组件拿到的 childRef.value 里就没有这些属性。

6.3 选项式子组件不用 defineExpose

选项式 API 的子组件,没有 defineExpose;父组件用 ref 拿到子组件时,ref.value 就是子组件的实例 proxy,可以直接访问子组件的 datamethods 等(相当于选项式里的 this 上的内容)。


七、实例与生命周期

7.1 创建阶段

  • beforeCreate 之前:实例正在创建,datamethods 等还未初始化。
  • created(或 setup 执行完):实例已创建好,datacomputedmethods(或 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

createdsetupthis.$elthis.$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 会令组件和“谁用我”强耦合,不利于复用;优先用 propsemitprovide/injectPinia


十、组件实例速查表

内容 选项式 API
访问当前实例 this 无 this;用 getCurrentInstance()?.proxy 仅高级场景
数据/方法 this.xxx 直接变量名、函数名
父访问子 refthis.$refs.xxx 即子实例 defineExpose;父 refxxxRef.value 为暴露对象
实例创建完成 created 之后 setup 执行完
$el、$refs 可用 mounted 之后 onMounted 里用 ref

十一、学习建议

  1. 先理解:一个组件标签 = 一个组件实例,实例上有 data、methods 和 $ 系列属性。
  2. 在选项式里熟练用 this 访问数据和 $refs$emit$nextTick;在 里用变量和 defineEmitsref 等,不依赖 this。
  3. 父要调子:子用 defineExpose 暴露方法/数据,父用 ref 拿到并调用;子要调父:用 emit
  4. 需要“当前实例”时再查 getCurrentInstance(),并注意其使用限制。

把本文档里的“父 ref + 子 defineExpose”和“子 emit”示例在项目里各做一遍,会掌握得更牢。更多内置属性见《Vue3内置属性.md》。祝你学习顺利。

发表评论