Vue 组件通信与自定义指令实战技巧
Orion K Lv6

在 Vue 开发中,组件通信和自定义指令是构建复杂应用的重要技能。本文将深入探讨 Vue 3 中各种组件通信方式的使用场景和最佳实践,以及如何创建实用的自定义指令来提升开发效率和用户体验。

Vue 3 组件通信全景图

1. Props 和 Emits(父子通信)

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 父组件 -->
<template>
<div>
<UserCard
:user="currentUser"
:editable="isAdmin"
@update-user="handleUserUpdate"
@delete-user="handleUserDelete"
/>
</div>
</template>

<script setup>
import { ref } from 'vue'
import UserCard from './UserCard.vue'

const currentUser = ref({
id: 1,
name: 'John Doe',
email: 'john@example.com',
role: 'user'
})

const isAdmin = ref(true)

function handleUserUpdate(updatedUser) {
currentUser.value = { ...currentUser.value, ...updatedUser }
console.log('用户信息已更新:', updatedUser)
}

function handleUserDelete(userId) {
console.log('删除用户:', userId)
// 执行删除逻辑
}
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
<!-- 子组件 UserCard.vue -->
<template>
<div class="user-card">
<div class="user-info">
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
<span class="role-badge" :class="user.role">{{ user.role }}</span>
</div>

<div v-if="editable" class="actions">
<button @click="editUser">编辑</button>
<button @click="deleteUser" class="danger">删除</button>
</div>

<!-- 编辑模态框 -->
<div v-if="showEditModal" class="modal">
<div class="modal-content">
<h4>编辑用户</h4>
<form @submit.prevent="saveUser">
<input v-model="editForm.name" placeholder="姓名" required>
<input v-model="editForm.email" type="email" placeholder="邮箱" required>
<select v-model="editForm.role">
<option value="user">普通用户</option>
<option value="admin">管理员</option>
</select>
<div class="modal-actions">
<button type="submit">保存</button>
<button type="button" @click="cancelEdit">取消</button>
</div>
</form>
</div>
</div>
</div>
</template>

<script setup>
import { ref, reactive } from 'vue'

// Props 定义
const props = defineProps({
user: {
type: Object,
required: true,
validator: (user) => {
return user && typeof user.id !== 'undefined' && user.name && user.email
}
},
editable: {
type: Boolean,
default: false
}
})

// Emits 定义
const emit = defineEmits({
'update-user': (user) => {
// 验证事件参数
return user && typeof user === 'object'
},
'delete-user': (userId) => {
return typeof userId === 'number' || typeof userId === 'string'
}
})

const showEditModal = ref(false)
const editForm = reactive({
name: '',
email: '',
role: 'user'
})

function editUser() {
// 初始化编辑表单
Object.assign(editForm, props.user)
showEditModal.value = true
}

function saveUser() {
// 发送更新事件
emit('update-user', { ...editForm })
showEditModal.value = false
}

function cancelEdit() {
showEditModal.value = false
}

function deleteUser() {
if (confirm('确定要删除这个用户吗?')) {
emit('delete-user', props.user.id)
}
}
</script>

2. v-model 双向绑定

自定义组件的 v-model

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
<!-- 自定义输入组件 -->
<template>
<div class="custom-input">
<label v-if="label">{{ label }}</label>
<input
:value="modelValue"
@input="updateValue"
:type="type"
:placeholder="placeholder"
:disabled="disabled"
:class="{ error: hasError }"
>
<span v-if="hasError" class="error-message">{{ errorMessage }}</span>
</div>
</template>

<script setup>
import { computed } from 'vue'

const props = defineProps({
modelValue: [String, Number],
label: String,
type: {
type: String,
default: 'text'
},
placeholder: String,
disabled: Boolean,
validator: Function,
errorMessage: String
})

const emit = defineEmits(['update:modelValue'])

const hasError = computed(() => {
if (props.validator && props.modelValue) {
return !props.validator(props.modelValue)
}
return false
})

function updateValue(event) {
let value = event.target.value

// 类型转换
if (props.type === 'number') {
value = value === '' ? null : Number(value)
}

emit('update:modelValue', value)
}
</script>

多个 v-model 绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
<!-- 用户表单组件 -->
<template>
<form class="user-form">
<div class="form-group">
<label>姓名</label>
<input
:value="firstName"
@input="emit('update:firstName', $event.target.value)"
>
</div>

<div class="form-group">
<label>姓氏</label>
<input
:value="lastName"
@input="emit('update:lastName', $event.target.value)"
>
</div>

<div class="form-group">
<label>邮箱</label>
<input
:value="email"
@input="emit('update:email', $event.target.value)"
type="email"
>
</div>
</form>
</template>

<script setup>
defineProps(['firstName', 'lastName', 'email'])
const emit = defineEmits(['update:firstName', 'update:lastName', 'update:email'])
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<!-- 使用多个 v-model -->
<template>
<div>
<UserForm
v-model:first-name="user.firstName"
v-model:last-name="user.lastName"
v-model:email="user.email"
/>

<p>完整姓名: {{ fullName }}</p>
</div>
</template>

<script setup>
import { reactive, computed } from 'vue'
import UserForm from './UserForm.vue'

const user = reactive({
firstName: '',
lastName: '',
email: ''
})

const fullName = computed(() => `${user.firstName} ${user.lastName}`.trim())
</script>

3. Provide/Inject(跨层级通信)

基础用法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!-- 根组件或祖先组件 -->
<template>
<div class="app">
<ThemeToggle />
<UserProfile />
<ProductList />
</div>
</template>

<script setup>
import { provide, ref, readonly } from 'vue'
import ThemeToggle from './ThemeToggle.vue'
import UserProfile from './UserProfile.vue'
import ProductList from './ProductList.vue'

// 主题状态
const theme = ref('light')
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// 用户状态
const currentUser = ref({
id: 1,
name: 'John Doe',
avatar: '/avatars/john.jpg',
permissions: ['read', 'write']
})

// 全局配置
const appConfig = ref({
apiBaseUrl: 'https://api.example.com',
version: '1.0.0',
features: {
darkMode: true,
notifications: true
}
})

// 提供数据和方法
provide('theme', {
current: readonly(theme),
toggle: toggleTheme
})

provide('user', readonly(currentUser))
provide('config', readonly(appConfig))

// 提供工具函数
provide('utils', {
formatDate: (date) => new Intl.DateTimeFormat('zh-CN').format(date),
formatCurrency: (amount) => new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount)
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<!-- 深层子组件 -->
<template>
<div class="product-card" :class="themeClass">
<h3>{{ product.name }}</h3>
<p class="price">{{ formatCurrency(product.price) }}</p>
<p class="date">发布时间: {{ formatDate(product.createdAt) }}</p>

<button
v-if="canEdit"
@click="editProduct"
class="edit-btn"
>
编辑
</button>
</div>
</template>

<script setup>
import { inject, computed } from 'vue'

const props = defineProps(['product'])

// 注入依赖
const { current: theme } = inject('theme')
const user = inject('user')
const { formatDate, formatCurrency } = inject('utils')

// 计算属性
const themeClass = computed(() => `theme-${theme.value}`)
const canEdit = computed(() => user.value.permissions.includes('write'))

function editProduct() {
console.log('编辑产品:', props.product.id)
}
</script>

响应式 Provide/Inject

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// composables/useGlobalState.js
import { ref, provide, inject, readonly } from 'vue'

const GLOBAL_STATE_KEY = Symbol('globalState')

// 创建全局状态
export function createGlobalState() {
const state = ref({
user: null,
notifications: [],
settings: {
theme: 'light',
language: 'zh-CN'
}
})

const actions = {
setUser(user) {
state.value.user = user
},

addNotification(notification) {
state.value.notifications.push({
id: Date.now(),
timestamp: new Date(),
...notification
})
},

removeNotification(id) {
const index = state.value.notifications.findIndex(n => n.id === id)
if (index > -1) {
state.value.notifications.splice(index, 1)
}
},

updateSettings(newSettings) {
state.value.settings = { ...state.value.settings, ...newSettings }
}
}

provide(GLOBAL_STATE_KEY, {
state: readonly(state),
...actions
})

return { state, ...actions }
}

// 使用全局状态
export function useGlobalState() {
const globalState = inject(GLOBAL_STATE_KEY)

if (!globalState) {
throw new Error('useGlobalState must be used within a provider')
}

return globalState
}

4. 事件总线(Event Bus)

创建类型安全的事件总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// utils/eventBus.js
import { ref } from 'vue'

class EventBus {
constructor() {
this.events = new Map()
}

// 订阅事件
on(event, callback) {
if (!this.events.has(event)) {
this.events.set(event, [])
}
this.events.get(event).push(callback)

// 返回取消订阅函数
return () => this.off(event, callback)
}

// 取消订阅
off(event, callback) {
if (this.events.has(event)) {
const callbacks = this.events.get(event)
const index = callbacks.indexOf(callback)
if (index > -1) {
callbacks.splice(index, 1)
}
}
}

// 发送事件
emit(event, ...args) {
if (this.events.has(event)) {
this.events.get(event).forEach(callback => {
try {
callback(...args)
} catch (error) {
console.error(`事件处理器错误 [${event}]:`, error)
}
})
}
}

// 一次性订阅
once(event, callback) {
const unsubscribe = this.on(event, (...args) => {
callback(...args)
unsubscribe()
})
return unsubscribe
}

// 清除所有事件
clear() {
this.events.clear()
}
}

// 创建全局事件总线
export const eventBus = new EventBus()

// 事件类型定义(TypeScript)
export const EventTypes = {
USER_LOGIN: 'user:login',
USER_LOGOUT: 'user:logout',
NOTIFICATION_SHOW: 'notification:show',
NOTIFICATION_HIDE: 'notification:hide',
THEME_CHANGE: 'theme:change',
CART_ADD_ITEM: 'cart:addItem',
CART_REMOVE_ITEM: 'cart:removeItem'
}

在组件中使用事件总线

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<!-- 购物车组件 -->
<template>
<div class="cart">
<h3>购物车 ({{ items.length }})</h3>
<div v-for="item in items" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<span>{{ item.price }}</span>
<button @click="removeItem(item.id)">移除</button>
</div>
</div>
</template>

<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
import { eventBus, EventTypes } from '@/utils/eventBus'

const items = ref([])
let unsubscribers = []

function addItem(item) {
items.value.push(item)
}

function removeItem(itemId) {
const index = items.value.findIndex(item => item.id === itemId)
if (index > -1) {
const removedItem = items.value.splice(index, 1)[0]
eventBus.emit(EventTypes.CART_REMOVE_ITEM, removedItem)
}
}

onMounted(() => {
// 订阅添加商品事件
unsubscribers.push(
eventBus.on(EventTypes.CART_ADD_ITEM, addItem)
)
})

onUnmounted(() => {
// 清理事件订阅
unsubscribers.forEach(unsubscribe => unsubscribe())
})
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<!-- 产品列表组件 -->
<template>
<div class="product-list">
<div v-for="product in products" :key="product.id" class="product">
<h4>{{ product.name }}</h4>
<p>{{ product.price }}</p>
<button @click="addToCart(product)">加入购物车</button>
</div>
</div>
</template>

<script setup>
import { eventBus, EventTypes } from '@/utils/eventBus'

const props = defineProps(['products'])

function addToCart(product) {
// 发送添加到购物车事件
eventBus.emit(EventTypes.CART_ADD_ITEM, {
id: product.id,
name: product.name,
price: product.price,
quantity: 1
})

// 显示通知
eventBus.emit(EventTypes.NOTIFICATION_SHOW, {
type: 'success',
message: `${product.name} 已加入购物车`
})
}
</script>

自定义指令实战

1. 基础自定义指令

v-focus 自动聚焦指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// directives/focus.js
export const vFocus = {
mounted(el, binding) {
// 延迟聚焦,确保 DOM 完全渲染
if (binding.value !== false) {
setTimeout(() => {
el.focus()
}, 0)
}
},

updated(el, binding) {
// 当绑定值变化时重新聚焦
if (binding.value && !binding.oldValue) {
el.focus()
}
}
}

v-click-outside 点击外部指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// directives/clickOutside.js
export const vClickOutside = {
mounted(el, binding) {
el._clickOutsideHandler = (event) => {
// 检查点击是否在元素外部
if (!(el === event.target || el.contains(event.target))) {
// 调用绑定的函数
if (typeof binding.value === 'function') {
binding.value(event)
}
}
}

// 延迟添加事件监听器,避免立即触发
setTimeout(() => {
document.addEventListener('click', el._clickOutsideHandler)
}, 0)
},

unmounted(el) {
// 清理事件监听器
if (el._clickOutsideHandler) {
document.removeEventListener('click', el._clickOutsideHandler)
delete el._clickOutsideHandler
}
}
}

2. 高级自定义指令

v-lazy 图片懒加载指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// directives/lazy.js
const defaultOptions = {
threshold: 0.1,
rootMargin: '50px 0px'
}

export const vLazy = {
mounted(el, binding) {
const options = { ...defaultOptions, ...binding.arg }

// 创建 Intersection Observer
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target
const src = binding.value

// 创建新的图片对象预加载
const imageLoader = new Image()

imageLoader.onload = () => {
// 加载成功后设置 src
img.src = src
img.classList.add('lazy-loaded')
img.classList.remove('lazy-loading')
}

imageLoader.onerror = () => {
// 加载失败时设置默认图片
img.src = '/images/placeholder-error.jpg'
img.classList.add('lazy-error')
img.classList.remove('lazy-loading')
}

// 添加加载中样式
img.classList.add('lazy-loading')

// 开始加载图片
imageLoader.src = src

// 停止观察该元素
observer.unobserve(img)
}
})
}, options)

// 设置初始状态
el.classList.add('lazy-image')
el.src = el.dataset.placeholder || '/images/placeholder.jpg'

// 开始观察
observer.observe(el)

// 保存 observer 引用以便清理
el._lazyObserver = observer
},

updated(el, binding) {
// 如果 src 改变,重新开始懒加载
if (binding.value !== binding.oldValue) {
el.classList.remove('lazy-loaded', 'lazy-error')
el._lazyObserver.observe(el)
}
},

unmounted(el) {
// 清理 observer
if (el._lazyObserver) {
el._lazyObserver.disconnect()
delete el._lazyObserver
}
}
}

v-permission 权限控制指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// directives/permission.js
import { useUserStore } from '@/stores/user'

export const vPermission = {
mounted(el, binding) {
checkPermission(el, binding)
},

updated(el, binding) {
checkPermission(el, binding)
}
}

function checkPermission(el, binding) {
const userStore = useUserStore()
const { value: requiredPermissions, arg: mode = 'some' } = binding

if (!requiredPermissions) {
console.warn('v-permission 指令需要权限参数')
return
}

const permissions = Array.isArray(requiredPermissions)
? requiredPermissions
: [requiredPermissions]

const userPermissions = userStore.permissions || []

let hasPermission = false

if (mode === 'every') {
// 需要拥有所有权限
hasPermission = permissions.every(permission =>
userPermissions.includes(permission)
)
} else {
// 需要拥有任一权限(默认)
hasPermission = permissions.some(permission =>
userPermissions.includes(permission)
)
}

if (!hasPermission) {
// 移除元素或隐藏
if (binding.modifiers.remove) {
el.remove()
} else {
el.style.display = 'none'
}
} else {
el.style.display = ''
}
}

v-loading 加载状态指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
// directives/loading.js
import { createApp } from 'vue'
import LoadingComponent from '@/components/Loading.vue'

export const vLoading = {
mounted(el, binding) {
createLoadingInstance(el, binding)
},

updated(el, binding) {
if (binding.value !== binding.oldValue) {
if (binding.value) {
showLoading(el, binding)
} else {
hideLoading(el)
}
}
},

unmounted(el) {
hideLoading(el)
}
}

function createLoadingInstance(el, binding) {
const loadingText = binding.arg || '加载中...'
const size = binding.modifiers.small ? 'small' :
binding.modifiers.large ? 'large' : 'medium'

// 创建 loading 组件实例
const loadingApp = createApp(LoadingComponent, {
text: loadingText,
size: size
})

const loadingEl = document.createElement('div')
loadingEl.className = 'v-loading-container'

// 挂载组件
loadingApp.mount(loadingEl)

// 保存引用
el._loadingInstance = {
app: loadingApp,
element: loadingEl
}

// 如果初始值为 true,显示 loading
if (binding.value) {
showLoading(el, binding)
}
}

function showLoading(el, binding) {
const { element } = el._loadingInstance

// 设置容器样式
const originalPosition = getComputedStyle(el).position
if (originalPosition === 'static') {
el.style.position = 'relative'
}

// 添加 loading 遮罩
element.style.position = 'absolute'
element.style.top = '0'
element.style.left = '0'
element.style.width = '100%'
element.style.height = '100%'
element.style.backgroundColor = 'rgba(255, 255, 255, 0.8)'
element.style.display = 'flex'
element.style.alignItems = 'center'
element.style.justifyContent = 'center'
element.style.zIndex = '1000'

el.appendChild(element)
}

function hideLoading(el) {
if (el._loadingInstance) {
const { element } = el._loadingInstance
if (element.parentNode) {
element.parentNode.removeChild(element)
}
}
}

3. 指令的注册和使用

全局注册指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// main.js
import { createApp } from 'vue'
import App from './App.vue'

// 导入自定义指令
import { vFocus } from './directives/focus'
import { vClickOutside } from './directives/clickOutside'
import { vLazy } from './directives/lazy'
import { vPermission } from './directives/permission'
import { vLoading } from './directives/loading'

const app = createApp(App)

// 注册全局指令
app.directive('focus', vFocus)
app.directive('click-outside', vClickOutside)
app.directive('lazy', vLazy)
app.directive('permission', vPermission)
app.directive('loading', vLoading)

app.mount('#app')

在组件中使用指令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
<template>
<div class="demo-page">
<!-- 自动聚焦 -->
<input v-focus placeholder="自动聚焦的输入框">

<!-- 点击外部关闭 -->
<div class="dropdown" v-click-outside="closeDropdown">
<button @click="showDropdown = !showDropdown">下拉菜单</button>
<ul v-if="showDropdown" class="dropdown-menu">
<li>选项 1</li>
<li>选项 2</li>
<li>选项 3</li>
</ul>
</div>

<!-- 图片懒加载 -->
<div class="image-gallery">
<img
v-for="image in images"
:key="image.id"
v-lazy="image.src"
:data-placeholder="image.placeholder"
:alt="image.alt"
class="gallery-image"
>
</div>

<!-- 权限控制 -->
<button v-permission="['admin', 'editor']" @click="deleteItem">
删除(需要管理员或编辑权限)
</button>

<button v-permission:every="['admin', 'super']" @click="systemConfig">
系统配置(需要管理员和超级权限)
</button>

<!-- 加载状态 -->
<div v-loading="isLoading" class="content-area">
<p>这里是内容区域</p>
<button @click="loadData">加载数据</button>
</div>
</div>
</template>

<script setup>
import { ref } from 'vue'

const showDropdown = ref(false)
const isLoading = ref(false)

const images = ref([
{
id: 1,
src: 'https://example.com/image1.jpg',
placeholder: '/images/placeholder.jpg',
alt: '图片1'
},
// 更多图片...
])

function closeDropdown() {
showDropdown.value = false
}

function deleteItem() {
console.log('删除操作')
}

function systemConfig() {
console.log('系统配置')
}

async function loadData() {
isLoading.value = true
try {
// 模拟 API 调用
await new Promise(resolve => setTimeout(resolve, 2000))
console.log('数据加载完成')
} finally {
isLoading.value = false
}
}
</script>

组合式函数封装通信逻辑

useEventBus 组合式函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// composables/useEventBus.js
import { onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'

export function useEventBus() {
const unsubscribers = []

const on = (event, callback) => {
const unsubscribe = eventBus.on(event, callback)
unsubscribers.push(unsubscribe)
return unsubscribe
}

const emit = (event, ...args) => {
eventBus.emit(event, ...args)
}

const once = (event, callback) => {
const unsubscribe = eventBus.once(event, callback)
unsubscribers.push(unsubscribe)
return unsubscribe
}

// 组件卸载时自动清理
onUnmounted(() => {
unsubscribers.forEach(unsubscribe => unsubscribe())
})

return {
on,
emit,
once
}
}

useParentChild 组合式函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// composables/useParentChild.js
import { getCurrentInstance, provide, inject } from 'vue'

const PARENT_CHILD_KEY = Symbol('parentChild')

export function useParent() {
const children = new Set()

const addChild = (child) => {
children.add(child)
}

const removeChild = (child) => {
children.delete(child)
}

const broadcastToChildren = (method, ...args) => {
children.forEach(child => {
if (child[method]) {
child[method](...args)
}
})
}

provide(PARENT_CHILD_KEY, {
addChild,
removeChild
})

return {
children,
broadcastToChildren
}
}

export function useChild() {
const instance = getCurrentInstance()
const parent = inject(PARENT_CHILD_KEY, null)

if (parent) {
parent.addChild(instance.proxy)

onUnmounted(() => {
parent.removeChild(instance.proxy)
})
}

return {
parent: parent ? parent : null
}
}

总结

Vue 3 提供了丰富的组件通信方式和强大的自定义指令系统,让我们能够构建复杂而灵活的应用。关键要点包括:

组件通信最佳实践:

  1. Props/Emits:适用于父子组件直接通信
  2. v-model:适用于双向数据绑定场景
  3. Provide/Inject:适用于跨层级的依赖注入
  4. Event Bus:适用于兄弟组件或远距离组件通信
  5. 状态管理:适用于复杂的全局状态管理

自定义指令最佳实践:

  1. 关注 DOM 操作:指令主要用于 DOM 相关的操作
  2. 生命周期管理:正确处理指令的挂载、更新和卸载
  3. 性能优化:避免在指令中进行重复的计算和 DOM 操作
  4. 错误处理:添加适当的错误处理和边界情况处理
  5. 可复用性:设计通用的指令,通过参数和修饰符提供灵活性

通过合理选择通信方式和创建实用的自定义指令,可以大大提升 Vue 应用的开发效率和用户体验。

本站由 提供部署服务