还记得被Vue 2组件访问支配的恐惧吗?$refs时不时给你个undefined惊喜,$parent和$children比亲戚关系还混乱,$emit和props传值传到你手软...别怕,Vue 3.0带着它的组合API来拯救你了!
作为一个Vue老司机,我曾经在组件访问这条路上踩过无数坑。最经典的就是那个$refs的坑——明明组件在那,偏偏告诉你undefined。后来才知道,要在mounted之后才能用,这种细节真是让人头大。
不过,Vue 3.0彻底改变了游戏规则!今天就带大家深度体验Vue 3.0在组件访问上的革新,保证让你直呼“真香”!
一、 Vue 2时代的“血泪史”:我们为什么需要改变?
先来回顾一下Vue 2的那些“祖传bug”:
1. $refs的薛定谔特性
// Vue 2的经典坑
export default {
mounted() {
// 有时候行,有时候不行,全看运气
this.$refs.myButton.click()
},
methods: {
handleClick() {
console.log('你猜我能不能被点击?')
}
}
}
2. $parent和$children的混沌关系
// 爸爸找儿子,儿子找爸爸,全家乱套
export default {
mounted() {
// 我是谁?我在哪?我要找哪个爸爸?
const parent = this.$parent
const children = this.$children
}
}
3. Event Bus的全局污染
// eventBus.js - 全局事件混乱的根源
import Vue from 'vue'
export const EventBus = new Vue()
// componentA.vue
EventBus.$emit('message', '你好!')
// componentZ.vue
EventBus.$on('message', (msg) => {
console.log(msg) // 等等,这消息是谁发的?
})
这些问题在Vue 3.0中都有了优雅的解决方案!
二、 Vue 3.0组件访问革命:组合API的降维打击
1. 模板引用(Template Refs):告别undefined的烦恼
Vue 3.0中,模板引用变得超级简单和可靠:
<template>
<div>
<!-- 普通元素的引用 -->
<input ref="inputRef" type="text" />
<!-- 组件引用 -->
<ChildComponent ref="childComponentRef" />
<button @click="handleClick">点击我</button>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import ChildComponent from './ChildComponent.vue'
// 创建引用 - 变量名必须与模板中的ref一致
const inputRef = ref(null)
const childComponentRef = ref(null)
// 现在你可以在任何地方安全使用!
const handleClick = () => {
// 直接操作DOM元素
if (inputRef.value) {
inputRef.value.focus()
}
// 调用子组件方法
if (childComponentRef.value) {
childComponentRef.value.someMethod()
}
}
// 不再需要等待mounted!
onMounted(() => {
console.log('组件挂载完成,引用已就绪!')
console.log(inputRef.value) // 肯定不是undefined!
})
</script>
看到了吗?不需要this,不需要担心时机,引用就像普通变量一样可靠!
2. defineExpose:精准控制组件暴露内容
Vue 3.0让你决定组件的哪些内容可以被外部访问,就像给组件加了隐私设置:
<!-- ChildComponent.vue -->
<template>
<div>
<p>{{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
const internalSecret = ref('这个外部访问不到')
const increment = () => {
count.value++
}
const reset = () => {
count.value = 0
}
// 明确暴露:只有这些可以被父组件访问
defineExpose({
count,
increment,
reset
// internalSecret 不会被暴露
})
</script>
父组件访问:
<template>
<ChildComponent ref="childRef" />
<button @click="callChildMethod">调用子组件方法</button>
</template>
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const childRef = ref(null)
const callChildMethod = () => {
if (childRef.value) {
childRef.value.increment() // 可以访问
console.log(childRef.value.count) // 可以访问
console.log(childRef.value.internalSecret) // undefined - 访问不到!
}
}
</script>
这就好比:以前你的组件是裸奔,现在可以穿上衣服,只露出想露的部分!
3. provide/inject:跨层级组件的直连通道
Vue 2的provide/inject已经不错,但Vue 3.0让它更好用:
<!-- 祖先组件 -->
<template>
<div>
<ParentComponent />
<button @click="updateData">更新数据</button>
</div>
</template>
<script setup>
import { provide, ref, readonly } from 'vue'
import ParentComponent from './ParentComponent.vue'
const globalData = ref('我是全局数据')
const secretData = ref('这个不想让后代修改')
// 提供数据
provide('globalData', globalData)
// 提供只读数据,防止后代修改
provide('secretData', readonly(secretData))
// 甚至提供方法
provide('updateGlobalData', (newValue) => {
globalData.value = newValue
})
const updateData = () => {
globalData.value = '更新后的数据'
}
</script>
后代组件使用:
<!-- 任意层级后代组件 -->
<template>
<div>
<p>{{ injectedData }}</p>
<button @click="updateData">修改数据</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
// 注入数据
const injectedData = inject('globalData')
const secretData = inject('secretData')
const updateFunction = inject('updateGlobalData')
// 注入默认值,防止未提供的情况
const optionalData = inject('optionalData', '默认值')
const updateData = () => {
updateFunction('后代组件修改的数据')
// secretData.value = '尝试修改' // 这个会失败,因为是只读的
}
</script>
三、 实战案例:TodoList应用的重构
让我们用一个具体的例子来看看Vue 3.0组件访问的强大之处:
Vue 2版本的TodoList(痛点版)
<!-- TodoList.vue - Vue 2版本 -->
<template>
<div>
<TodoInput @add-todo="addTodo" />
<TodoList :todos="todos" @delete-todo="deleteTodo" />
<TodoStatus :todos="todos" ref="statusRef" />
</div>
</template>
<script>
export default {
data() {
return {
todos: []
}
},
methods: {
addTodo(todo) {
this.todos.push(todo)
// 需要等待下一帧才能访问statusRef
this.$nextTick(() => {
this.$refs.statusRef.updateStatus()
})
},
deleteTodo(index) {
this.todos.splice(index, 1)
}
}
}
</script>
Vue 3.0版本的TodoList(优雅版)
<!-- TodoList.vue - Vue 3.0版本 -->
<template>
<div class="todo-app">
<h1>📝 我的待办清单</h1>
<TodoInput @add-todo="addTodo" />
<TodoList
:todos="todos"
@delete-todo="deleteTodo"
@toggle-todo="toggleTodo"
/>
<TodoStatus ref="statusRef" />
<!-- 直接操作状态组件 -->
<div class="actions">
<button @click="showStats">显示统计</button>
<button @click="resetAll">重置所有</button>
</div>
</div>
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
import TodoInput from './TodoInput.vue'
import TodoList from './TodoList.vue'
import TodoStatus from './TodoStatus.vue'
// 响应式数据
const todos = ref([])
const statusRef = ref(null)
// 提供数据给所有子组件
provide('todos', readonly(todos)) // 只读,防止意外修改
provide('todoActions', {
addTodo: (text) => {
todos.value.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
})
},
deleteTodo: (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
},
toggleTodo: (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
})
const addTodo = (text) => {
todos.value.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
})
}
const deleteTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
// 直接调用子组件方法
const showStats = () => {
if (statusRef.value) {
statusRef.value.showDetailedStats()
}
}
const resetAll = () => {
todos.value = []
if (statusRef.value) {
statusRef.value.reset()
}
}
</script>
状态组件:
<!-- TodoStatus.vue -->
<template>
<div class="todo-status">
<div class="stats">
<span>总计: {{ total }}</span>
<span>已完成: {{ completed }}</span>
<span>未完成: {{ pending }}</span>
</div>
<div v-if="showDetails" class="detailed-stats">
<h3>详细统计</h3>
<p>完成率: {{ completionRate }}%</p>
<p>最近添加: {{ recentTodos }} 个</p>
</div>
</div>
</template>
<script setup>
import { ref, computed, inject } from 'vue'
const todos = inject('todos')
const showDetails = ref(false)
const total = computed(() => todos.value.length)
const completed = computed(() => todos.value.filter(t => t.completed).length)
const pending = computed(() => total.value - completed.value)
const completionRate = computed(() => {
return total.value > 0 ? Math.round((completed.value / total.value) * 100) : 0
})
const recentTodos = computed(() => {
const oneDayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000)
return todos.value.filter(todo => new Date(todo.createdAt) > oneDayAgo).length
})
const showDetailedStats = () => {
showDetails.value = true
console.log('显示详细统计信息')
}
const reset = () => {
showDetails.value = false
console.log('状态组件已重置')
}
// 暴露给父组件的方法
defineExpose({
showDetailedStats,
reset,
completionRate
})
</script>
四、 性能优化和最佳实践
Vue 3.0的强大也意味着更多的责任,下面是一些实用建议:
1. 引用管理的黄金法则
// ✅ 好的做法
const inputRef = ref(null)
const onlyNeededWhen = ref(null)
// 条件性创建引用
const conditionalRef = (el) => {
if (el && someCondition) {
// 处理引用
}
}
// ❌ 避免的做法
const tooManyRefs = ref(null) // 引用所有东西
const unusedRefs = ref(null) // 创建了但不使用
2. provide/inject的TypeScript支持
<script setup lang="ts">
import { provide, inject, Ref } from 'vue'
// 提供时定义类型
interface Todo {
id: number
text: string
completed: boolean
}
interface TodoActions {
addTodo: (text: string) => void
deleteTodo: (id: number) => void
}
const todos = ref<Todo[]>([])
provide<Todo[]>('todos', todos)
provide<TodoActions>('todoActions', {
addTodo: (text: string) => { /* 实现 */ },
deleteTodo: (id: number) => { /* 实现 */ }
})
// 注入时指定类型和默认值
const injectedTodos = inject<Ref<Todo[]>>('todos', ref([]))
const injectedActions = inject<TodoActions>('todoActions', {
addTodo: () => console.warn('没有提供todoActions'),
deleteTodo: () => console.warn('没有提供todoActions')
})
</script>
五、 总结:为什么Vue 3.0是组件访问的终极解决方案?
经过上面的深度分析,我们可以看到Vue 3.0在组件访问方面的巨大进步:
- 更少的魔法:告别
this的混乱,拥抱明确的引用 - 更好的类型支持:TypeScript友好,开发体验大幅提升
- 更精准的控制:用
defineExpose决定暴露什么 - 更清晰的架构:
provide/inject让组件关系一目了然 - 更好的性能:组合API让代码更可优化
最重要的是,Vue 3.0让我们的代码更可预测、更易维护。再也不用担心那个经典的"Cannot read property 'xxx' of undefined"错误了!
现在就用起Vue 3.0的组合API吧,你会发现组件访问从此变得如此简单愉快。毕竟,谁不喜欢写既强大又优雅的代码呢?
示例代码完整可运行,建议在实际项目中体验这些新特性。Vue 3.0的组件访问方式,用过就回不去了!
Vue 3.0组件访问新方式解析

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



