Vue3.0父传子子传父的血和泪:一个菜鸟的踩坑实录
前言
最近在研究Vue3.0,本以为从Vue2升级过来应该很轻松,结果在父子组件通信这块栽了不少跟头。今天就来分享一下我在实现一个简单的编辑弹窗功能时遇到的各种问题,希望能帮到同样在Vue3.0路上挣扎的小伙伴们。
背景
我需要实现一个用户列表页面,包含编辑和删除功能。编辑功能通过弹窗实现,需要父组件传递数据给子组件,子组件编辑完成后回传给父组件。听起来很简单,对吧?但实际操作中遇到了N个坑...
坑一:表格作用域插槽获取不到数据
问题描述
一开始我的表格操作列是这样写的:
<el-table-column label="操作" width="150">
<template #default>
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
结果点击按钮时,scope.row 是 undefined,完全获取不到行数据。
问题原因
作用域插槽必须声明参数! 我写的是 <template #default>,没有声明 scope 参数,所以 scope 是 undefined。
解决方案
正确的写法应该是:
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
关键点: <template #default="scope"> 中的 scope 参数必须声明,这样才能获取到当前行的数据。
坑二:子组件表单数据无法编辑
问题描述
在子组件中,我一开始这样写:
<script setup>
const props = defineProps({
editData: {
type: Object,
default: () => ({})
}
})
const localEditData = ref({...props.editData})
</script>
<template>
<el-form :model="localEditData">
<el-form-item label="姓名">
<el-input v-model="localEditData.name" placeholder="请输入姓名" />
</el-form-item>
</el-form>
</template>
结果表单无法编辑,数据也无法正常回显。
问题原因
- 直接修改props报错:如果直接用 v-model="props.editData.name",Vue会报错,因为props是只读的。
- ref的响应性问题:用 ref({...props.editData}) 创建的对象,在props变化时不会自动更新。
解决方案
使用 reactive 创建本地副本,并用 watch 监听props变化:
<script setup>
import { reactive, watch } from 'vue'
const props = defineProps({
editData: {
type: Object,
default: () => ({})
}
})
// 使用reactive创建本地副本
const localEditData = reactive({
id: '',
name: '',
place: ''
})
// 监听props变化,同步到本地数据
watch(
() => props.editData,
(newVal) => {
Object.assign(localEditData, newVal)
},
{ deep: true, immediate: true }
)
</script>
坑三:为什么非要用reactive?用ref行不行?
我的疑问
既然 ref 也能创建响应式数据,为什么非要推荐用 reactive?
原因分析
- 对象解构问题:ref 创建的对象,解构后会失去响应性
- 深层响应性:reactive 对深层对象的响应性处理更好
- 性能考虑:对于复杂对象,reactive 性能更优
实际对比
// ❌ ref方式 - 可能有问题
const localEditData = ref({...props.editData})
// 如果props.editData是复杂对象,可能响应性不完整
// ✅ reactive方式 - 更稳定
const localEditData = reactive({
id: '',
name: '',
place: ''
})
// 明确初始化所有字段,响应性更可靠
坑四:为什么必须要监听?不用监听行不行?
我的疑问
既然父组件传递了数据,子组件为什么还要监听变化?
原因分析
Vue3的响应式系统特点:
- props变化不会自动同步:父组件传递新数据时,子组件的本地副本不会自动更新
- 弹窗复用问题:同一个弹窗组件可能编辑不同的数据
- 响应式更新:确保子组件能及时响应父组件的数据变化
实际场景
// 父组件中
const handleEdit = (data) => {
editItem.value = {...data} // 数据变化
editRef.value.editDialogVisible = true // 打开弹窗
}
// 如果没有watch,子组件的localEditData不会更新
// 弹窗显示的还是上一次的数据
坑五:deep监听很消耗性能,有替代方案吗?
watch(
() => props.editData,
(newVal) => {
Object.assign(localEditData, newVal)
},
{ deep: true, immediate: true } // deep: true 会深度监听,性能消耗大
)
替代方案
方案一:浅层监听 + 手动同步
watch(
() => props.editData,
(newVal) => {
// 手动同步需要的字段,避免深度监听
localEditData.id = newVal.id
localEditData.name = newVal.name
localEditData.place = newVal.place
},
{ immediate: true } // 去掉 deep: true
)
方案二:使用计算属性
const localEditData = computed(() => ({
...props.editData
}))
方案三:监听特定字段
watch(
() => [props.editData.id, props.editData.name, props.editData.place],
([id, name, place]) => {
localEditData.id = id
localEditData.name = name
localEditData.place = place
},
{ immediate: true }
)
坑六:父组件为什么要用展开运算符?
在父组件中,我一开始这样写:
const handleEdit = (data) => {
editItem.value = data // 直接赋值
editRef.value.editDialogVisible = true
}
结果发现,有时候弹窗显示的数据不是最新的。
问题原因
引用传递 vs 值传递:
- editItem.value = data 是引用传递,如果 data 是响应式对象,可能会有意外的副作用
- 如果 data 后续被修改,editItem 也会跟着变化
- Vue的响应式系统可能无法正确检测到这种变化
解决方案
使用展开运算符创建新对象:
const handleEdit = (data) => {
editItem.value = {...data} // 创建新对象,确保响应性
editRef.value.editDialogVisible = true
}
为什么这样做?
- 避免引用问题:创建新对象,避免意外的引用修改
- 确保响应性:新对象会触发Vue的响应式更新
- 数据隔离:子组件的修改不会影响原始数据
完整解决方案
经过以上踩坑,我最终实现的完整代码如下:
父组件vue
<template>
<div class="app">
<el-table :data="list" style="width: 100%">
<el-table-column type="index" label="序号" width="80"></el-table-column>
<el-table-column label="ID" prop="id"></el-table-column>
<el-table-column label="姓名" prop="name" width="150"></el-table-column>
<el-table-column label="籍贯" prop="place"></el-table-column>
<el-table-column label="操作" width="150">
<template #default="scope">
<el-button type="primary" link @click="handleEdit(scope.row)">编辑</el-button>
<el-button type="danger" link @click="handleDelete(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<EditDialog
ref="editRef"
:editData="editItem"
@edit-success="handleEditSuccess"
/>
</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import EditDialog from './components/EditDialog.vue'
import axios from 'axios'
const list = ref([])
const editItem = ref(null)
const editRef = ref(null)
onMounted(() => {
getList()
})
const getList = () => {
axios.get('/list').then(res => {
list.value = res.data
}).catch(err => {
console.error('获取列表失败:', err)
})
}
const handleEdit = (data) => {
// 关键:使用展开运算符创建新对象
editItem.value = {...data}
editRef.value.editDialogVisible = true
}
const handleEditSuccess = (data) => {
axios.patch(`/edit/${data.id}`, {
name: data.name,
place: data.place
}).then(() => {
getList()
}).catch(err => {
console.error('编辑失败:', err)
}).finally(() => {
editRef.value.editDialogVisible = false
})
}
</script>
子组件
<template>
<el-dialog v-model="editDialogVisible" title="编辑" width="400px">
<el-form label-width="50px" :model="localEditData">
<el-form-item label="姓名">
<el-input v-model="localEditData.name" placeholder="请输入姓名" />
</el-form-item>
<el-form-item label="籍贯">
<el-input v-model="localEditData.place" placeholder="请输入籍贯" />
</el-form-item>
</el-form>
<template #footer>
<span class="dialog-footer">
<el-button @click="editDialogVisible = false">取消</el-button>
<el-button type="primary" @click="emit('edit-success', localEditData)">确认</el-button>
</span>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive, defineProps, defineEmits, defineExpose, watch } from 'vue'
const editDialogVisible = ref(false)
const props = defineProps({
editData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['edit-success'])
defineExpose({editDialogVisible})
// 使用reactive创建本地副本
const localEditData = reactive({
id: '',
name: '',
place: ''
})
// 监听props变化,手动同步字段(避免deep监听)
watch(
() => props.editData,
(newVal) => {
if (newVal && newVal.id) {
localEditData.id = newVal.id
localEditData.name = newVal.name
localEditData.place = newVal.place
}
},
{ immediate: true }
)
</script>
如果不是单纯为了练习props,还有另一种方式
父组件可以直接在打开editDialog时,直接通过函数传参的形式,把要修改的一行数据传入子组件
const handleEdit = (data) => {
// 这个我做了下更新,vue3.0讲究专人专事,所以修改dialogvisibile的事情还是让子组件自己干吧
editRef.value.openDialog(data)
}
子组件先初始化一个form表单数据
// 表单数据
const form = ref({
name: '',
place: '',
id: ''
})
然后在打开弹窗这个openDialog方法里接收一个row参数,并将其赋值给form
// 打开弹框
const openDialog = (row) => {
editDialogVisible.value = true
form.value = {...row}
}
vue3.0子组件的属性和方法默认是不对父组件公开的,我们要使用dedineExpose方法使其对外公开
// 向父组件暴露打开弹窗的方法,专人专事
defineExpose({ openDialog })
专人专事,所以编辑也就在编辑弹框里做了
// 向父组件传递编辑完成
const emit = defineEmits(['edit-success'])
// 编辑
const update = () => {
axios.patch(`/edit/${form.value.id}`, {
name: form.value.name,
place: form.value.place
})
emit('edit-success', form.value)
editDialogVisible.value = false
}
父组件使用子组件也就变成了下面这样
<Edit ref="editRef" @edit-success="getList" />
总结
Vue3.0的父子组件通信看似简单,但实际开发中会遇到各种细节问题:
- 作用域插槽必须声明参数:<template #default="scope">
- 表单编辑需要本地副本:用 reactive 创建本地数据
- props变化需要监听:用 watch 同步数据变化
- 避免深度监听:手动同步字段,提升性能
- 使用展开运算符:确保响应性和数据隔离
这些坑虽然让人头疼,但踩过之后对Vue3.0的理解会更深入。希望我的踩坑经历能帮到正在Vue3.0路上奋斗的小伙伴们!
记住:在Vue3.0的世界里,细节决定成败! 🎯*