7. 组件通信
7.1 Props
vue
<!-- 父组件 -->
<template>
<ChildComponent
:title="title"
:user="user"
:count="count"
@update-count="updateCount"
/>
</template>
<script setup>
import { ref } from 'vue'
const title = ref('子组件标题')
const user = ref({ name: 'John', age: 30 })
const count = ref(0)
const updateCount = (newCount) => {
count.value = newCount
}
</script>
<!-- 子组件 -->
<script setup>
// 定义 props
const props = defineProps({
// 基础类型
title: String,
// 多种类型
count: [Number, String],
// 对象类型
user: {
type: Object,
default: () => ({}) // 对象默认值必须是工厂函数
},
// 自定义验证器
age: {
type: Number,
validator: (value) => {
return value >= 0 && value <= 150
}
}
})
// 在模板中直接使用 props
</script>
7.2 自定义事件 (Emits)
vue
<!-- 子组件 -->
<template>
<button @click="emitEvent">触发事件</button>
</template>
<script setup>
// 定义 emits(数组形式)
const emit = defineEmits(['update:title', 'custom-event'])
// 定义 emits(对象形式,带验证)
const emit = defineEmits({
'update:title': (value) => {
// 验证逻辑
return typeof value === 'string' && value.length > 0
},
'custom-event': null // 不验证
})
const emitEvent = () => {
// 触发事件
emit('update:title', '新标题')
emit('custom-event', { data: '额外数据' })
}
</script>
<!-- 父组件 -->
<template>
<ChildComponent
@update:title="updateTitle"
@custom-event="handleCustom"
/>
</template>
7.3 v-model 双向绑定
vue
<!-- 自定义 v-model 组件 -->
<template>
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
/>
</template>
<script setup>
defineProps(['modelValue'])
defineEmits(['update:modelValue'])
</script>
<!-- 使用(默认 v-model) -->
<CustomInput v-model="message" />
<!-- 等同于 -->
<CustomInput
:modelValue="message"
@update:modelValue="newValue => message = newValue"
/>
<!-- 多个 v-model -->
<template>
<input :value="firstName" @input="$emit('update:firstName', $event.target.value)" />
<input :value="lastName" @input="$emit('update:lastName', $event.target.value)" />
</template>
<script setup>
defineProps(['firstName', 'lastName'])
defineEmits(['update:firstName', 'update:lastName'])
</script>
<!-- 使用多个 v-model -->
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
7.4 透传 Attributes
vue
<!-- 子组件 -->
<template>
<!-- 使用 $attrs 绑定所有透传属性 -->
<div v-bind="$attrs">
子组件内容
</div>
</template>
<script setup>
import { useAttrs } from 'vue'
// 访问透传的 attributes
const attrs = useAttrs()
console.log(attrs.class) // 父组件传入的 class
console.log(attrs.style) // 父组件传入的 style
// 禁用 Attributes 继承
defineOptions({
inheritAttrs: false
})
</script>
7.5 插槽 (Slots)
vue
<!-- 子组件 -->
<template>
<div class="container">
<!-- 默认插槽 -->
<slot>默认内容</slot>
<!-- 具名插槽 -->
<slot name="header">默认头部</slot>
<!-- 作用域插槽 -->
<slot name="footer" :user="user" :data="data"></slot>
<!-- 动态插槽名 -->
<slot :name="dynamicSlotName"></slot>
</div>
</template>
<!-- 父组件使用 -->
<template>
<ChildComponent>
<!-- 默认插槽内容 -->
<p>这是默认插槽内容</p>
<!-- 具名插槽 -->
<template #header>
<h1>自定义头部</h1>
</template>
<!-- 作用域插槽 -->
<template #footer="{ user, data }">
<p>用户: {{ user.name }}</p>
<p>数据: {{ data }}</p>
</template>
<!-- 简写 -->
<template v-slot:header>简写头部</template>
<!-- 动态插槽 -->
<template #[dynamicSlotName]>
动态插槽内容
</template>
</ChildComponent>
</template>
7.6 provide / inject
vue
<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'
const count = ref(0)
const user = ref({ name: 'John' })
// 提供数据
provide('count', count)
provide('user', user)
// 提供方法
provide('increment', () => {
count.value++
})
// 提供响应式对象
provide('theme', {
primaryColor: '#409EFF',
fontSize: '14px'
})
</script>
<!-- 后代组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据
const count = inject('count')
const user = inject('user')
const increment = inject('increment')
// 带默认值
const theme = inject('theme', {
primaryColor: '#000000',
fontSize: '16px'
})
// 工厂函数
const config = inject('config', () => ({ apiUrl: '/api' }))
// 注入作为响应式数据
const userRef = inject('user')
userRef.value.name = '新名字' // 会修改祖先组件的数据
</script>
8. 高级组件模式
8.1 异步组件
javascript
// 基本异步组件
const AsyncComponent = defineAsyncComponent(() =>
import('./MyComponent.vue')
)
// 带选项的异步组件
const AsyncComponentWithOptions = defineAsyncComponent({
// 加载函数
loader: () => import('./MyComponent.vue'),
// 加载中显示的组件
loadingComponent: LoadingComponent,
// 加载失败显示的组件
errorComponent: ErrorComponent,
// 延迟显示 loading 的时间
delay: 200,
// 超时时间
timeout: 3000,
// 是否可挂起
suspensible: true,
// 加载错误时的回调
onError(error, retry, fail, attempts) {
if (error.message.match(/fetch/) && attempts <= 3) {
retry() // 重试
} else {
fail() // 失败
}
}
})
8.2 动态组件
vue
<template>
<!-- 动态组件 -->
<component :is="currentComponent" />
<!-- 动态组件 with props -->
<component
:is="currentComponent"
:title="title"
@custom-event="handleEvent"
/>
<!-- 保持动态组件状态 -->
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</template>
<script setup>
import { ref, shallowRef } from 'vue'
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
const currentComponent = ref('ComponentA')
const components = {
ComponentA,
ComponentB
}
// 使用 shallowRef 避免不必要的响应式转换
const currentComponent = shallowRef(ComponentA)
</script>
8.3 递归组件
vue
<!-- TreeItem.vue -->
<template>
<li>
<div @click="toggle">
{{ node.name }}
<span v-if="hasChildren">[{{ isOpen ? '-' : '+' }}]</span>
</div>
<ul v-if="hasChildren && isOpen">
<TreeItem
v-for="child in node.children"
:key="child.id"
:node="child"
/>
</ul>
</li>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
node: Object
})
const isOpen = ref(false)
const hasChildren = computed(() =>
props.node.children && props.node.children.length > 0
)
const toggle = () => {
if (hasChildren.value) {
isOpen.value = !isOpen.value
}
}
// 递归组件必须要有 name
defineOptions({
name: 'TreeItem'
})
</script>
8.4 函数式组件
vue
<!-- 函数式组件 -->
<script>
// 选项式 API
export default {
functional: true,
props: ['level'],
render(h, context) {
return h(
'h' + context.props.level,
context.data,
context.children
)
}
}
</script>
<!-- 组合式 API 函数式组件 -->
<script setup>
import { h } from 'vue'
const FunctionalComponent = (props, { slots, attrs, emit }) => {
return h('div', [
h('span', `Count: ${props.count}`),
slots.default?.(),
slots.header?.(),
])
}
// 定义 props
FunctionalComponent.props = ['count']
// 定义 emits
FunctionalComponent.emits = ['click']
</script>
9. 内置组件
9.1 Transition & TransitionGroup
vue
<template>
<!-- Transition -->
<button @click="show = !show">切换</button>
<Transition name="fade">
<p v-if="show">你好</p>
</Transition>
<!-- 自定义类名 -->
<Transition
enter-from-class="enter-from"
enter-active-class="enter-active"
enter-to-class="enter-to"
leave-from-class="leave-from"
leave-active-class="leave-active"
leave-to-class="leave-to"
>
<p v-if="show">自定义动画</p>
</Transition>
<!-- JavaScript 钩子 -->
<Transition
@before-enter="onBeforeEnter"
@enter="onEnter"
@after-enter="onAfterEnter"
@enter-cancelled="onEnterCancelled"
@before-leave="onBeforeLeave"
@leave="onLeave"
@after-leave="onAfterLeave"
@leave-cancelled="onLeaveCancelled"
>
<p v-if="show">JavaScript 动画</p>
</Transition>
<!-- TransitionGroup -->
<TransitionGroup name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</TransitionGroup>
</template>
<style scoped>
/* 基础过渡 */
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
/* 列表过渡 */
.list-move, /* 对移动中的元素应用的过渡 */
.list-enter-active,
.list-leave-active {
transition: all 0.5s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
/* 确保离开的项目从布局中删除 */
.list-leave-active {
position: absolute;
}
</style>
9.2 KeepAlive
vue
<template>
<!-- 基本用法 -->
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
<!-- 包含/排除组件 -->
<KeepAlive :include="['ComponentA', 'ComponentB']" :exclude="['ComponentC']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 最大缓存数 -->
<KeepAlive :max="10">
<component :is="currentComponent" />
</KeepAlive>
</template>
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 组件被激活时调用
console.log('组件激活')
})
onDeactivated(() => {
// 组件被停用时调用
console.log('组件停用')
})
</script>
9.3 Teleport
vue
<template>
<!-- 传送到 body -->
<Teleport to="body">
<div class="modal">
<p>这是一个模态框</p>
<button @click="show = false">关闭</button>
</div>
</Teleport>
<!-- 传送到指定元素 -->
<Teleport to="#modal-container">
<Modal />
</Teleport>
<!-- 条件传送 -->
<Teleport :disabled="isMobile">
<Modal />
</Teleport>
<!-- 多个 Teleport 到一样目标 -->
<Teleport to="#modals">
<ModalA />
</Teleport>
<Teleport to="#modals">
<ModalB />
</Teleport>
<!-- 结果:ModalA 在前,ModalB 在后 -->
</template>
9.4 Suspense
vue
<template>
<!-- 基本用法 -->
<Suspense>
<!-- 默认插槽:异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- fallback 插槽:加载状态 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
<!-- 多个异步组件 -->
<Suspense>
<template #default>
<div>
<AsyncComponent1 />
<AsyncComponent2 />
</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./AsyncComponent.vue')
)
// 组件中的异步 setup
const AsyncComponentWithAsyncSetup = defineAsyncComponent({
async setup() {
const data = await fetchData()
return { data }
}
})
</script>
10. 自定义指令
10.1 全局自定义指令
javascript
// main.js
const app = createApp(App)
// 注册全局指令
app.directive('focus', {
// 指令绑定元素挂载前
beforeMount(el) {
el.style.backgroundColor = 'yellow'
},
// 指令绑定元素挂载后
mounted(el) {
el.focus()
},
// 组件更新前
beforeUpdate(el) {
console.log('beforeUpdate')
},
// 组件更新后
updated(el) {
console.log('updated')
},
// 组件卸载前
beforeUnmount(el) {
console.log('beforeUnmount')
},
// 组件卸载后
unmounted(el) {
console.log('unmounted')
}
})
// 简写形式(只在 mounted 和 updated 时调用)
app.directive('color', (el, binding) => {
el.style.color = binding.value
})
10.2 局部自定义指令
vue
<script setup>
// 局部自定义指令
const vFocus = {
mounted: (el) => el.focus()
}
// 对象形式
const vCustom = {
beforeMount(el, binding, vnode, prevVnode) {
// el: 绑定元素
// binding: 绑定对象
// vnode: 当前 VNode
// prevVnode: 上一个 VNode
}
}
// 函数形式
const vColor = (el, binding) => {
el.style.color = binding.value
}
</script>
<template>
<!-- 使用指令 -->
<input v-focus />
<p v-color="'red'">红色文字</p>
<!-- 动态参数 -->
<p v-custom:[arg]="value">自定义指令</p>
<!-- 修饰符 -->
<div v-demo.modifier></div>
</template>
<script setup>
// 访问修饰符
const vDemo = {
mounted(el, binding) {
console.log(binding.modifiers) // { modifier: true }
}
}
</script>
10.3 指令钩子参数
javascript
const vMyDirective = {
// 元素属性或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {},
// 元素插入 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 元素插入 DOM 后调用
mounted(el, binding, vnode, prevVnode) {
// binding 对象包含:
// value: 指令的值
// oldValue: 之前的值
// arg: 指令参数
// modifiers: 修饰符对象
// instance: 组件实例
// dir: 指令定义对象
},
// 组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 组件更新后调用
updated(el, binding, vnode, prevVnode) {},
// 组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
10.4 实用指令示例
javascript
// 1. 点击外部关闭指令
const vClickOutside = {
mounted(el, { value: callback }) {
el.clickOutsideHandler = (event) => {
if (!el.contains(event.target)) {
callback(event)
}
}
document.addEventListener('click', el.clickOutsideHandler)
},
unmounted(el) {
document.removeEventListener('click', el.clickOutsideHandler)
}
}
// 2. 防抖指令
const vDebounce = {
mounted(el, { value: callback, arg: delay = 300 }) {
let timer
el.addEventListener('input', (event) => {
clearTimeout(timer)
timer = setTimeout(() => {
callback(event)
}, delay)
})
}
}
// 3. 权限指令
const vPermission = {
mounted(el, { value: permission }) {
const userPermissions = ['read', 'write'] // 从 store 获取
if (!userPermissions.includes(permission)) {
el.parentNode?.removeChild(el)
}
}
}
// 4. 复制指令
const vCopy = {
mounted(el, { value }) {
el.addEventListener('click', () => {
navigator.clipboard.writeText(value)
.then(() => alert('复制成功'))
.catch(err => console.error('复制失败:', err))
})
}
}
这是 Vue 3 核心知识的第二部分,涵盖了组件通信、高级组件模式、内置组件和自定义指令。
© 版权声明
文章版权归作者所有,未经允许请勿转载。
相关文章
暂无评论...