[Medium] 📄 Component Communication
1. What are the ways for Vue components to communicate with each other?
Vue 組件之間有哪些溝通方式?
Vue 組件之間的資料傳遞是開發中非常常見的需求,根據組件之間的關係不同,有多種溝通方式可以選擇。
組件關係分類
父子組件:props / $emit
祖孫組件:provide / inject
兄弟組件:Event Bus / Vuex / Pinia
任意組件:Vuex / Pinia
1. Props(父傳子)
用途:父組件向子組件傳遞資料
<!-- ParentComponent.vue - Vue 3 <script setup> -->
<template>
<div>
<h1>父組件</h1>
<ChildComponent
:message="parentMessage"
:user="userInfo"
:count="counter"
/>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const parentMessage = ref('Hello from parent');
const userInfo = ref({
name: 'John',
age: 30,
});
const counter = ref(0);
</script>
<!-- ChildComponent.vue - Vue 3 <script setup> -->
<template>
<div>
<h2>子組件</h2>
<p>收到的訊息:{{ message }}</p>
<p>使用者:{{ user.name }}({{ user.age }} 歲)</p>
<p>計數:{{ count }}</p>
</div>
</template>
<script setup>
// 基本型別驗證
defineProps({
message: {
type: String,
required: true,
default: '',
},
// 物件型別驗證
user: {
type: Object,
required: true,
default: () => ({}),
},
// 數字型別驗證
count: {
type: Number,
default: 0,
validator: (value) => value >= 0, // 自訂驗證:必須 >= 0
},
});
</script>
Props 的注意事項
<!-- Vue 3 <script setup> 寫法 -->
<script setup>
import { ref, onMounted } from 'vue';
const props = defineProps({
message: String,
});
const localMessage = ref(props.message);
onMounted(() => {
// ❌ 錯誤:不應該直接修改 props
// props.message = 'new value'; // 會產生警告
// ✅ 正確:已經在上方將 props 複製到 ref
localMessage.value = props.message;
});
</script>
2. $emit(子傳父)
用途:子組件向父組件發送事件與資料
<!-- ChildComponent.vue - Vue 3 <script setup> -->
<template>
<div>
<button @click="sendToParent">發送給父組件</button>
<input v-model="inputValue" @input="handleInput" />
</div>
</template>
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['custom-event', 'update:modelValue']);
const inputValue = ref('');
const sendToParent = () => {
// 發送事件給父組件
emit('custom-event', {
message: 'Hello from child',
timestamp: Date.now(),
});
};
const handleInput = () => {
// 即時發送輸入值
emit('update:modelValue', inputValue.value);
};
</script>
<!-- ParentComponent.vue - Vue 3 <script setup> -->
<template>
<div>
<h1>父組件</h1>
<!-- 監聽子組件的事件 -->
<ChildComponent
@custom-event="handleCustomEvent"
@update:modelValue="handleUpdate"
/>
<p>收到的資料:{{ receivedData }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const receivedData = ref(null);
const handleCustomEvent = (data) => {
console.log('收到子組件的事件:', data);
receivedData.value = data;
};
const handleUpdate = (value) => {
console.log('輸入值更新:', value);
};
</script>
Vue 3 的 emits 選項
<!-- Vue 3 <script setup> 寫法 -->
<script setup>
const emit = defineEmits({
// 聲明會發送的事件
'custom-event': null,
// 帶驗證的事件
'update:modelValue': (value) => {
if (typeof value !== 'string') {
console.warn('modelValue 必須是字串');
return false;
}
return true;
},
});
const sendEvent = () => {
emit('custom-event', 'data');
};
</script>
3. v-model(雙向綁定)
用途:父子組件之間的雙向資料綁定
Vue 2 的 v-model
<!-- ParentComponent.vue -->
<template>
<custom-input v-model="message" />
<!-- 等同於 -->
<custom-input :value="message" @input="message = $event" />
</template>
<!-- CustomInput.vue (Vue 2) -->
<template>
<input :value="value" @input="$emit('input', $event.target.value)" />
</template>
<script>
export default {
props: ['value'],
};
</script>
Vue 3 的 v-model
<!-- ParentComponent.vue - Vue 3 <script setup> -->
<template>
<custom-input v-model="message" />
<!-- 等同於 -->
<custom-input :modelValue="message" @update:modelValue="message = $event" />
</template>
<script setup>
import { ref } from 'vue';
import CustomInput from './CustomInput.vue';
const message = ref('');
</script>
<!-- CustomInput.vue - Vue 3 <script setup> -->
<template>
<input :value="modelValue" @input="updateValue" />
</template>
<script setup>
defineProps({
modelValue: String,
});
const emit = defineEmits(['update:modelValue']);
const updateValue = (event) => {
emit('update:modelValue', event.target.value);
};
</script>
Vue 3 的多個 v-model
<!-- ParentComponent.vue - Vue 3 <script setup> -->
<template>
<user-form v-model:name="userName" v-model:email="userEmail" />
</template>
<script setup>
import { ref } from 'vue';
import UserForm from './UserForm.vue';
const userName = ref('');
const userEmail = ref('');
</script>
<!-- UserForm.vue - Vue 3 <script setup> -->
<template>
<div>
<input
:value="name"
@input="$emit('update:name', $event.target.value)"
placeholder="姓名"
/>
<input
:value="email"
@input="$emit('update:email', $event.target.value)"
placeholder="信箱"
/>
</div>
</template>
<script setup>
defineProps({
name: String,
email: String,
});
defineEmits(['update:name', 'update:email']);
</script>
4. Provide / Inject(祖孫組件)
用途:跨層級的組件通訊,避免逐層傳遞 props
<!-- GrandparentComponent.vue -->
<template>
<div>
<h1>祖父組件</h1>
<parent-component />
</div>
</template>
<script>
import { ref, provide } from 'vue';
import ParentComponent from './ParentComponent.vue';
export default {
components: { ParentComponent },
setup() {
const userInfo = ref({
name: 'John',
role: 'admin',
});
const updateUser = (newInfo) => {
userInfo.value = { ...userInfo.value, ...newInfo };
};
// 提供資料和方法給後代組件
provide('userInfo', userInfo);
provide('updateUser', updateUser);
return { userInfo };
},
};
</script>
<!-- ParentComponent.vue -->
<template>
<div>
<h2>父組件(不使用 inject)</h2>
<child-component />
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
};
</script>
<!-- ChildComponent.vue -->
<template>
<div>
<h3>子組件</h3>
<p>使用者:{{ userInfo.name }}</p>
<p>角色:{{ userInfo.role }}</p>
<button @click="changeUser">修改使用者</button>
</div>
</template>
<script>
import { inject } from 'vue';
export default {
setup() {
// 注入祖父組件提供的資料
const userInfo = inject('userInfo');
const updateUser = inject('updateUser');
const changeUser = () => {
updateUser({ name: 'Jane', role: 'user' });
};
return {
userInfo,
changeUser,
};
},
};
</script>
Provide / Inject 的注意事項
<script>
import { ref, readonly, provide } from 'vue';
export default {
setup() {
const state = ref({ count: 0 });
// ❌ 錯誤:後代組件可以直接修改
provide('state', state);
// ✅ 正確:提供唯讀資料和修改方法
provide('state', readonly(state));
provide('updateState', (newState) => {
state.value = newState;
});
},
};
</script>
5. $refs(父訪問子)
用途:父組件直接存取子組件的屬性和方法
<!-- ParentComponent.vue -->
<template>
<div>
<child-component ref="childRef" />
<button @click="callChildMethod">呼叫子組件方法</button>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
components: { ChildComponent },
methods: {
callChildMethod() {
// 直接呼叫子組件的方法
this.$refs.childRef.someMethod();
// 存取子組件的資料
console.log(this.$refs.childRef.someData);
},
},
mounted() {
// ✅ 在 mounted 後才能存取 $refs
console.log(this.$refs.childRef);
},
};
</script>
<!-- ChildComponent.vue -->
<script>
export default {
data() {
return {
someData: 'Child data',
};
},
methods: {
someMethod() {
console.log('子組件的方法被呼叫');
},
},
};
</script>
Vue 3 Composition API 的 ref
<template>
<child-component ref="childRef" />
<button @click="callChild">呼叫子組件</button>
</template>
<script setup>
import { ref } from 'vue';
import ChildComponent from './ChildComponent.vue';
const childRef = ref(null);
const callChild = () => {
childRef.value.someMethod();
};
</script>
6. $parent / $root(子訪問父)
用途:子組件存取父組件或根組件(不建議使用)
<!-- ChildComponent.vue -->
<script>
export default {
mounted() {
// 存取父組件
console.log(this.$parent.someData);
this.$parent.someMethod();
// 存取根組件
console.log(this.$root.globalData);
},
};
</script>
⚠️ 不建議使用的原因:
- 增加組件之間的耦合度
- 難以追蹤資料流向
- 不利於組件重用