ref
?屬性?與?props
一、核心概念對比
特性 | ref ?(標簽屬性) | props |
---|---|---|
作用對象 | DOM 元素/組件實例 | 組件間數據傳遞 |
數據流向 | 父組件訪問子組件/DOM | 父組件 → 子組件 |
響應性 | 直接操作對象 | 單向數據流(只讀) |
使用場景 | 獲取 DOM/調用子組件方法 | 組件參數傳遞 |
Vue3 變化 | 不再自動數組形式 | 需要?defineProps ?聲明 |
二、ref
?屬性的深度解析
1. 基礎用法
<!-- 獲取 DOM 元素 -->
<template><input ref="inputRef" type="text"><ChildComponent ref="childRef" />
</template><script setup>
import { ref, onMounted } from 'vue'// DOM 引用
const inputRef = ref(null)// 組件引用
const childRef = ref(null)onMounted(() => {inputRef.value.focus() // 操作 DOMchildRef.value.sayHello() // 調用子組件方法
})
</script>
2. 組件引用規范
// 子組件 ChildComponent.vue
<script setup>
// 必須暴露方法才能被父組件調用
defineExpose({sayHello: () => console.log('Hello from child!'),childData: ref('子組件數據')
})
</script>
3. 類型安全(TypeScript)
// 父組件中定義組件引用類型
import ChildComponent from './ChildComponent.vue'const childRef = ref<InstanceType<typeof ChildComponent>>()
三、props
?的響應式處理
1. 基礎聲明
<!-- 父組件 -->
<ChildComponent :title="parentTitle" /><!-- 子組件 -->
<script setup>
const props = defineProps({title: {type: String,required: true}
})
</script>
2. 保持響應性
// 正確方式:使用 toRef
import { toRef } from 'vue'const title = toRef(props, 'title')// 錯誤!直接解構會失去響應性
// const { title } = props
四、聯合使用場景
場景:表單驗證組件
<!-- 父組件 -->
<template><ValidationForm ref="formRef" :rules="validationRules"@submit="handleSubmit"/>
</template><script setup>
const formRef = ref(null)
const validationRules = ref({/* 驗證規則 */})// 調用子組件方法
const validateForm = () => {formRef.value.validate()
}
</script>
<!-- 子組件 ValidationForm.vue -->
<script setup>
defineProps(['rules'])const validate = () => {// 執行驗證邏輯
}// 暴露方法給父組件
defineExpose({ validate })
</script>
五、核心差異總結
對比維度 | ref ?(標簽屬性) | props |
---|---|---|
數據方向 | 父 → 子(操作子組件/DOM) | 父 → 子(數據傳遞) |
可修改性 | 可直接修改子組件暴露的內容 | 只讀(需通過事件通知父組件修改) |
響應式機制 | 直接引用對象 | 需要?toRef ?保持響應性 |
典型應用 | DOM操作/調用子組件方法 | 組件參數配置 |
組合式 API | 通過?ref() ?創建引用 | 通過?defineProps ?聲明 |
六、最佳實踐指南
1.?ref
?使用原則
-
最小化暴露:只暴露必要的組件方法/數據
-
避免直接修改:不要通過?
ref
?直接修改子組件狀態 -
配合 TypeScript:使用類型定義確保安全訪問
2.?props
?使用規范
-
只讀原則:始終視?
props
?為不可變數據 -
響應式轉換:使用?
toRef
?處理需要響應式的?props
-
明確驗證:始終定義?
props
?的類型驗證
七、常見問題解決
Q1: 為什么通過?ref
?訪問子組件屬性得到?undefined
?
原因:子組件未通過?defineExpose
?暴露屬性
解決方案:
// 子組件
defineExpose({publicMethod: () => {/* ... */}
})
Q2: 如何同時使用?ref
?和?v-for
?
<template><ChildComponent v-for="item in list" :key="item.id":ref="setItemRef"/>
</template><script setup>
const itemRefs = ref([])const setItemRef = el => {if (el) itemRefs.value.push(el)
}
</script>
Q3: 如何類型安全地組合?ref
?和?props
?
// 父組件
import ChildComponent from './ChildComponent.vue'type ChildComponentExpose = {validate: () => boolean
}const childRef = ref<ChildComponentExpose>()// 子組件
defineExpose<ChildComponentExpose>({validate: () => true
})
八、綜合應用示例
父組件:
<template><UserFormref="userForm":user-data="formData"@submit="handleSubmit"/><button @click="validateForm">驗證表單</button>
</template><script setup lang="ts">
import { ref } from 'vue'
import UserForm from './UserForm.vue'type UserFormExpose = {validate: () => booleanresetForm: () => void
}const userForm = ref<UserFormExpose>()
const formData = ref({ name: '', email: '' })const validateForm = () => {if (userForm.value?.validate()) {console.log('表單驗證通過')}
}const handleSubmit = (data) => {console.log('提交數據:', data)
}
</script>
子組件 UserForm.vue:
<template><form @submit.prevent="submitForm"><input v-model="localData.name"><input v-model="localData.email"><button type="submit">提交</button></form>
</template><script setup lang="ts">
import { ref, toRefs } from 'vue'const props = defineProps<{userData: {name: stringemail: string}
}>()const emit = defineEmits(['submit'])// 本地副本(避免直接修改 props)
const localData = ref({ ...props.userData })const validate = () => {return localData.value.name.length > 0 && localData.value.email.includes('@')
}const resetForm = () => {localData.value = { name: '', email: '' }
}const submitForm = () => {emit('submit', localData.value)
}defineExpose({validate,resetForm
})
</script>
關鍵總結:
-
ref
?屬性:用于直接訪問 DOM/子組件實例,需要配合?defineExpose
?使用 -
props
:用于父組件向子組件傳遞數據,需保持只讀特性 -
協作模式:
-
父組件通過?
props
?傳遞數據 -
子組件通過事件通知父組件
-
必要時通過?
ref
?調用子組件方法
-
-
類型安全:使用 TypeScript 類型定義確保可靠訪問
事件傳遞
在 Vue3 中,子組件向父組件傳遞數據主要通過?事件機制?實現。以下是 5 種主要實現方式及其使用場景:
一、基礎事件傳遞 (推薦)
實現方式:
子組件觸發自定義事件 → 父組件監聽事件
<!-- 子組件 ChildComponent.vue -->
<script setup>
const emit = defineEmits(['sendData']) // 聲明事件const sendToParent = () => {emit('sendData', { message: 'Hello from child!' }) // 觸發事件
}
</script><template><button @click="sendToParent">發送數據</button>
</template>
<!-- 父組件 ParentComponent.vue -->
<template><ChildComponent @send-data="handleData" />
</template><script setup>
const handleData = (payload) => {console.log(payload.message) // 輸出:Hello from child!
}
</script>
最佳實踐:
-
使用?
kebab-case
?事件名(如?send-data
) -
通過 TypeScript 定義事件類型:
const emit = defineEmits<{(e: 'sendData', payload: { message: string }): void }>()
二、v-model 雙向綁定 (表單場景推薦)
實現原理:
v-model
?是?:modelValue
?+?@update:modelValue
?的語法糖
<!-- 子組件 InputComponent.vue -->
<script setup>
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])const updateValue = (e) => {emit('update:modelValue', e.target.value)
}
</script><template><input :value="modelValue" @input="updateValue">
</template>
<!-- 父組件 -->
<template><InputComponent v-model="inputValue" />
</template><script setup>
const inputValue = ref('')
</script>
多 v-model 綁定:
<ChildComponent v-model:name="userName"v-model:age="userAge"
/>
三、異步回調模式 (需要返回值時)
適用場景:需要等待父組件處理結果的異步操作
<!-- 子組件 -->
<script setup>
const emit = defineEmits(['request'])const handleClick = async () => {const response = await emit('request', { id: 123 })console.log('父組件返回:', response)
}
</script>
<!-- 父組件 -->
<template><ChildComponent @request="handleRequest" />
</template><script setup>
const handleRequest = async (payload) => {const data = await fetchData(payload.id)return data // 返回給子組件
}
</script>
四、Expose 方法調用 (需要直接訪問子組件)
<!-- 子組件 -->
<script setup>
const childData = ref('子組件數據')defineExpose({getData: () => childData.value,updateData: (newVal) => childData.value = newVal
})
</script>
<!-- 父組件 -->
<template><ChildComponent ref="childRef" />
</template><script setup>
const childRef = ref(null)const getChildData = () => {console.log(childRef.value?.getData()) // 輸出:子組件數據childRef.value?.updateData('新數據')
}
</script>
五、狀態管理方案 (跨組件通信)
適用場景:多層嵌套組件或兄弟組件通信
// store/counter.js
import { reactive } from 'vue'export const counterStore = reactive({count: 0,increment() {this.count++}
})
<!-- 子組件 -->
<script setup>
import { counterStore } from './store/counter'const updateCount = () => {counterStore.increment()
}
</script>
<!-- 父組件 -->
<script setup>
import { counterStore } from './store/counter'
</script><template>當前計數:{{ counterStore.count }}
</template>
方法對比表
方法 | 適用場景 | 優點 | 缺點 |
---|---|---|---|
基礎事件傳遞 | 簡單數據傳遞 | 直觀明確 | 多層嵌套時需逐層傳遞 |
v-model 綁定 | 表單輸入組件 | 語法簡潔 | 僅適用簡單雙向綁定 |
異步回調模式 | 需要父組件響應結果 | 支持異步交互 | 邏輯復雜度稍高 |
Expose 方法 | 需要直接操作子組件 | 靈活性強 | 破壞組件封裝性 |
狀態管理 | 跨組件/復雜場景 | 解耦組件關系 | 需要額外學習成本 |
最佳實踐指南
-
優先使用事件傳遞:保持組件獨立性
-
復雜場景用狀態管理:Pinia 是 Vue3 官方推薦方案
-
v-model 用于表單:保持雙向綁定的簡潔性
-
避免濫用 ref:防止組件過度耦合
-
TypeScript 類型定義:
// 事件類型定義 defineEmits<{(e: 'updateData', payload: { id: number }): void(e: 'cancel'): void }>()// Props 類型定義 defineProps<{userId: numberuserName: string }>()
完整示例:購物車組件交互
<template><div class="cart-item"><span>{{ item.name }}</span><input type="number" :value="item.quantity"@input="updateQuantity($event.target.value)"><button @click="emit('remove', item.id)">刪除</button></div>
</template><!-- 子組件 CartItem.vue -->
<script setup>
const props = defineProps({item: {type: Object,required: true}
})const emit = defineEmits(['update:quantity', 'remove'])const updateQuantity = (newQty) => {emit('update:quantity', {id: props.item.id, qty: newQty})
}
</script>
<template><CartItem v-for="item in cartItems":key="item.id":item="item"@update:quantity="handleUpdate"@remove="handleRemove"/>
</template><!-- 父組件 ShoppingCart.vue -->
<script setup>
const cartItems = ref([{ id: 1, name: '商品A', quantity: 2 },{ id: 2, name: '商品B', quantity: 1 }
])const handleUpdate = ({ id, qty }) => {const item = cartItems.value.find(i => i.id === id)if (item) item.quantity = Number(qty)
}const handleRemove = (id) => {cartItems.value = cartItems.value.filter(i => i.id !== id)
}
</script>