一、说说 Vue 3 的 setup 函数执行时机?如果要在 setup 里访问 this,应该怎么办?
(一) setup 函数的执行时机
setup 函数是 在组件实例被完全创建之前、在 beforeCreate 和 created 生命周期钩子之间 同步执行的。
更具体的时间线可以这样理解:
- 初始化组件实例:Vue 开始创建组件实例(初始化组件实例的属性和事件等)。
- 执行
setup函数:在data、computed、methods等选项被解析或执行之前,setup函数就被立即调用了。 - 执行生命周期钩子:
-
setup执行完毕后,紧接着会执行beforeCreate钩子。- 然后,组件的
data、computed等选项被处理。 - 最后,执行
created钩子。
关键点:
- 同步执行:
setup是同步的,不是异步的。 - “this” 尚未创建:因为在
setup被调用时,组件的其他选项(如data、methods)都还未被处理,所以组件实例 “this” 在setup内部是undefined。 - 在
beforeCreate之前:你可以在beforeCreate钩子中访问到setup函数返回的对象属性,这证明了setup执行得更早。
(二) 在 setup 里访问 this 的正确方法
简短回答:你不应该,也不需要,也无法在 setup 中访问 this。
Vue 3 的 Composition API 的设计理念就是通过函数参数来获取组件上下文,而不是依赖神秘的 this。setup 函数提供了两个参数来让你访问原本需要通过 this 访问的东西:
setup 函数的参数:
props:第一个参数,是响应式的,包含组件接收的所有 prop。你不能使用 ES6 解构它,这会破坏其响应性。如果需要解构,应使用 toRefs 或 toRef。
import { toRefs } from 'vue'
export default {
props: ['title'],
setup(props) {
// 将 `props` 转为一个其中每个 property 都是 ref 的对象
const { title } = toRefs(props)
console.log(title.value) // 访问值需要用 .value
// 或者直接访问
console.log(props.title)
}
}
context:第二个参数,是一个普通的 JavaScript 对象,它暴露了三个在组件中非常常用的 property。它不是响应式的,因此你可以安全地使用 ES6 解构。
export default {
setup(props, context) {
// 或者直接解构
// setup(props, { attrs, slots, emit, expose })
// Attribute(非响应式的对象,等价于 $attrs)
console.log(context.attrs)
// 插槽(非响应式的对象,等价于 $slots)
console.log(context.slots)
// 触发事件(函数,等价于 $emit)
console.log(context.emit)
// 暴露公共 property(函数,用于被父组件通过模板 ref 访问时暴露特定值)
console.log(context.expose)
}
}
如何替代 this 的功能?
通过 props 和 context,你可以完全取代在 Options API 中通过 this 访问的所有功能:
| Options API (通过 ) |
Composition API (在 中) |
|
|
(第一个参数) |
|
|
|
|
|
|
|
|
|
|
|
使用 (不推荐) |
|
|
使用 (不推荐) |
|
|
模板 ref:使用 函数声明 |
关于 getCurrentInstance():
这是一个高级 API,主要用于库的开发者。它返回当前的组件实例,类似于 this。强烈不推荐在应用代码中使用它,因为它破坏了 Composition API 的封装性和可测试性,并且你的代码在服务端渲染 (SSR) 时可能会出错。所有标准用例都应该通过 props 和 context 来解决。
(三) 总结
- 执行时机:
setup在组件实例创建之初、beforeCreate之前同步执行。此时this毫无意义。 - 访问
this:不要在setup里尝试访问this,它是undefined。 - 正确方法:使用
setup函数的两个参数props和context来获取组件上下文、属性、插槽以及触发事件。这是 Vue 3 Composition API 设计的标准模式。
二、Vue 3 组件通信的方式有哪些?分别适用于哪些场景?
Vue 3 的组件通信方式非常丰富,可以根据组件关系和使用场景灵活选择。下面我将它们分类并详细说明其适用场景。
(一) 父子组件通信 (Props & Events)
这是最基础、最常用的通信方式,遵循 单向数据流 原则。
- 父传子:Props
-
- 方式:父组件通过属性(Attributes)将数据传递给子组件,子组件使用
defineProps来声明和接收。 - 适用场景:
- 方式:父组件通过属性(Attributes)将数据传递给子组件,子组件使用
-
-
- 父组件需要向子组件传递初始数据或配置。
- 子组件需要根据父组件的数据渲染内容(如列表项、表单数据)。
- 任何需要明确数据来源和流向的场景,遵循单向数据流。
-
- 子传父:自定义事件 (Emits)
-
- 方式:子组件通过
defineEmits定义事件,并通过emit('event-name', payload)触发。父组件使用v-on或@来监听子组件触发的事件。 - 适用场景:
- 方式:子组件通过
-
-
- 子组件发生了某个动作(如按钮点击、表单提交),需要通知父组件。
- 子组件需要让父组件来修改传递下来的 Prop(通过让父组件监听事件并更新自己的数据)。
- 子组件想要向上传递数据(如输入框的值、选择的结果)。
-
代码示例:
<!-- 父组件 Parent.vue -->
<template>
<Child :title="parentTitle" @update-title="parentTitle = $event" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './Child.vue'
const parentTitle = ref('Hello from Parent')
</script>
<!-- 子组件 Child.vue -->
<template>
<h1>{
{ title }}</h1>
<button @click="updateTitle('New Title!')">Change Title</button>
</template>
<script setup>
// 接收 Props
defineProps(['title'])
// 声明事件
const emit = defineEmits(['update-title'])
const updateTitle = (newTitle) => {
emit('update-title', newTitle) // 触发事件并传递数据
}
</script>
(二) 跨层级组件通信 (Provide / Inject)
用于祖先组件向后代组件(无论层级多深)传递数据,避免“Prop 逐级透传”的麻烦。
- 方式:祖先组件使用
provide(key, value)提供数据,任何后代组件都可以使用inject(key)来注入获取数据。 - 适用场景:
-
- 全局主题/样式配置(如主题色、语言包)。
- 当前用户信息,在多个深层嵌套组件中都需要使用。
- 第三方插件,Vue Router、Pinia 等库的内部实现就利用了 Provide/Inject。
- 复杂的多层表单或组件结构,其中底层组件需要访问顶层组件的数据或方法。
代码示例:
<!-- 祖先组件 Ancestor.vue -->
<script setup>
import { provide, ref } from 'vue'
const userLocation = ref('North Pole')
// 提供静态值或响应式 ref
provide('location', userLocation)
</script>
<!-- 深层后代组件 Descendant.vue -->
<script setup>
import { inject } from 'vue'
// 注入祖先提供的数据,第二个参数是默认值(可选)
const userLocation = inject('location', 'Earth')
</script>
(三) 兄弟组件通信 (通过共同的父组件 / 状态管理)
两个兄弟组件之间不能直接通信,需要一个共同的“桥梁”。
- 通过共同的父组件(Props + Events)
-
- 方式:将数据提升到共同的父组件中管理。组件A通过事件通知父组件修改数据,父组件再通过Prop将更新后的数据传递给组件B。
- 适用场景:
-
-
- 简单的应用,兄弟组件通信不频繁。
- 组件结构不深,提升状态到父组件不会导致逻辑过于复杂。
-
- 使用状态管理库 (Pinia)
-
- 方式:将需要共享的状态抽取到 Pinia Store 中。任何组件(无论关系如何)都可以导入并使用或修改这个 Store 中的状态。
- 适用场景:
-
-
- 多个不相关的组件需要共享同一状态。
- 中大型复杂应用,需要集中式、可预测的状态管理。
- 需要持久化、序列化或调试(如时间旅行)的状态。
- 兄弟组件通信频繁,通过父组件传递显得冗杂和难以维护。
-
(四) 任意组件通信 (状态管理 Pinia)
这是最强大和灵活的方式,彻底解耦了组件和它们所依赖的状态。
- 方式:定义一个 Pinia Store,在其中管理状态和业务逻辑。任何组件都可以导入并使用这个 Store。
- 适用场景:
-
- 用户登录状态、个人信息、权限等全局数据。
- 购物车,需要在不同页面和组件中同步商品信息。
- 复杂的、跨多组件和多页面的数据流。
- 替代 Vuex 的最佳选择。
代码示例:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({ count: 0 }),
actions: {
increment() {
this.count++
}
}
})
<!-- 任意组件 AnyComponent.vue -->
<script setup>
import { useCounterStore } from '@/stores/counter'
const counter = useCounterStore()
</script>
<template>
<button @click="counter.increment()">{
{ counter.count }}</button>
</template>
(五) 模板引用 (Template Refs) 和 组件实例方法 (defineExpose)
用于父组件直接访问子组件实例的属性或方法。
- 方式:
-
- 子组件使用
<script setup>时,默认是关闭的。需要使用defineExpose显式暴露属性或方法。 - 父组件使用
ref声明一个同名的模板 ref,并通过ref.value来访问。
- 子组件使用
- 适用场景:
-
- 父组件需要直接调用子组件的方法(如
focus(),validate())。 - 需要直接操作子组件的 DOM 元素(如管理焦点、触发动画)。
- 应谨慎使用,因为它破坏了组件的封装性。优先考虑使用 Props 和 Events 进行通信。
- 父组件需要直接调用子组件的方法(如
代码示例:
<!-- 子组件 Child.vue -->
<script setup>
import { ref } from 'vue'
const input = ref(null)
const publicMethod = () => { console.log('Called!') }
// 只有暴露出去的,父组件才能访问到
defineExpose({
publicMethod,
input
})
</script>
<!-- 父组件 Parent.vue -->
<template>
<Child ref="childRef" />
</template>
<script setup>
import { ref, onMounted } from 'vue'
import Child from './Child.vue'
const childRef = ref(null)
onMounted(() => {
// 调用子组件暴露的方法
childRef.value.publicMethod()
// 访问子组件内部的 DOM 元素
childRef.value.input.focus()
})
</script>
(六) 事件总线 (Event Bus) - 已不推荐
- 方式:创建一个新的 Vue 应用实例作为中央事件总线,使用
$on,$emit,$off<

最低0.47元/天 解锁文章

被折叠的 条评论
为什么被折叠?



