第 1 节 概述
前几篇讲的 props 和事件,解决的是父子组件之间的通信。
但在真实项目中,组件层级往往很深:
1
2
3
4
| App.vue
└─ Layout.vue
└─ Header.vue
└─ UserAvatar.vue(需要用户信息)
|
如果只用 props,用户信息要从 App 一路传到 UserAvatar。中间组件不关心这个数据,只是帮忙传递。这叫做"prop 逐层传递"。
这一篇讲几个进阶工具:
- provide / inject:跨层级直接传数据
- 模板 ref:操作 DOM 或子组件
- defineExpose:暴露子组件内部
- v-model:在组件上实现双向绑定
第 2 节 provide / inject
2.1 解决 prop 逐层传递
provide 在祖先组件中提供数据,inject 在后代组件中接收:
1
2
3
4
5
6
7
| <!-- App.vue(祖先) -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const user = ref({ name: '张三', role: 'admin' })
provide('user', user)
</script>
|
1
2
3
4
5
6
7
8
9
10
| <!-- UserAvatar.vue(深层后代) -->
<script setup lang="ts">
import { inject } from 'vue'
const user = inject('user')
</script>
<template>
<p>{{ user.name }}</p>
</template>
|
不管隔了多少层,inject 都能直接拿到数据。
说明
provide 和 inject 之间的组件不需要做任何事,也不需要感知数据传递的存在。
2.2 保持响应式
provide 传递 ref 或 reactive 对象时,注入的数据也是响应式的:
1
2
3
4
| const count = ref(0)
provide('count', count)
// 后代修改 count,祖先的视图也会更新
|
但更好的做法是,把修改逻辑放在 provide 的组件里,只暴露操作函数给后代:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <script setup lang="ts">
import { provide, ref } from 'vue'
const count = ref(0)
function increment() {
count.value++
}
provide('count', {
count: readonly(count), // 防止子组件误修改
increment,
})
</script>
|
这样后代可以调用 increment 来修改 count,但不能直接写 count.value = xxx。
2.3 使用 Symbol 防止冲突
大型应用中,不同的 provide 可能用了相同的 key。可以用 Symbol 来避免冲突:
1
2
| // keys.ts
export const userKey = Symbol('user')
|
1
2
3
| import { userKey } from './keys'
provide(userKey, userData)
inject(userKey)
|
2.4 app-level provide
如果你的数据整个应用都需要,可以在 main.ts 中注入:
1
2
| const app = createApp(App)
app.provide('theme', 'dark')
|
所有组件都可以直接 inject('theme')。
第 3 节 模板 ref
3.1 获取 DOM 元素
有时你需要直接操作 DOM 元素,比如让输入框自动聚焦:
1
2
3
4
5
6
7
8
9
10
11
12
13
| <script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const input = useTemplateRef('my-input')
onMounted(() => {
input.value?.focus()
})
</script>
<template>
<input ref="my-input" type="text" />
</template>
|
ref 属性指定名称,useTemplateRef 用同一个名称获取 DOM 引用。
注意
模板 ref 必须在 onMounted 之后才能访问。在组件刚创建时 DOM 还不存在。
3.2 获取子组件实例
ref 也可以获取子组件实例:
1
2
3
4
5
6
7
8
9
10
11
12
13
| <template>
<ChildComponent ref="child" />
</template>
<script setup lang="ts">
import { useTemplateRef, onMounted } from 'vue'
const child = useTemplateRef('child')
onMounted(() => {
console.log(child.value) // 子组件实例
})
</script>
|
3.3 defineExpose
默认情况下,使用 <script setup> 的组件是私有的,父组件通过 ref 获取不到其内部的方法和数据。
用 defineExpose 显式暴露:
1
2
3
4
5
6
7
8
9
10
11
12
| <!-- Child.vue -->
<script setup lang="ts">
import { ref } from 'vue'
const count = ref(0)
function reset() {
count.value = 0
}
defineExpose({ count, reset })
</script>
|
1
2
3
4
5
6
7
8
9
10
| <!-- Parent.vue -->
<script setup lang="ts">
import { useTemplateRef } from 'vue'
const child = useTemplateRef('child')
function handleReset() {
child.value?.reset()
}
</script>
|
说明
defineExpose 应该用在确实需要父组件调用的场景,比如表单组件的校验方法。一般来说,能用 props 和事件解决的,就不用 defineExpose。
第 4 节 组件上的 v-model
4.1 语法糖原理
v-model 在组件上是 props + 事件 的语法糖:
1
| <ChildComponent v-model="searchText" />
|
等价于:
1
2
3
4
| <ChildComponent
:modelValue="searchText"
@update:modelValue="newValue => searchText = newValue"
/>
|
默认情况下,v-model 传递的 prop 名是 modelValue,更新事件是 update:modelValue。
4.2 在子组件中使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
| <!-- Child.vue -->
<script setup lang="ts">
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
function updateValue(event: Event) {
const target = event.target as HTMLInputElement
emit('update:modelValue', target.value)
}
</script>
<template>
<input :value="modelValue" @input="updateValue" />
</template>
|
4.3 自定义 v-model 名称
如果要多个双向绑定,可以给 v-model 命名:
1
2
3
4
| <ChildComponent
v-model:title="pageTitle"
v-model:content="pageContent"
/>
|
子组件接收:
1
2
3
4
| <script setup lang="ts">
defineProps(['title', 'content'])
defineEmits(['update:title', 'update:content'])
</script>
|
这一篇介绍了三个进阶工具。
但到目前为止,我们一直在讨论一个页面内部的事情。
如果应用有多个页面,怎么在页面之间切换?用户点击导航栏,内容区域怎么变?
这个问题的答案是路由(Router)。