注册
web

Vue3.0父传子子传父的血和泪:一个菜鸟的踩坑实录


,没有声明 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>

关键点:  中的 scope 参数必须声明,这样才能获取到当前行的数据。

坑二:子组件表单数据无法编辑


问题描述


在子组件中,我一开始这样写:




<template>
<el-form :model="localEditData">
<el-form-item label="姓名">
<el-input v-model="localEditData.name" placeholder="请输入姓名" />
el-form-item>

el-form>
template>

结果表单无法编辑,数据也无法正常回显。


问题原因



  1. 直接修改props报错:如果直接用 v-model="props.editData.name",Vue会报错,因为props是只读的。
  2. ref的响应性问题:用 ref({...props.editData}) 创建的对象,在props变化时不会自动更新。

解决方案


使用 reactive 创建本地副本,并用 watch 监听props变化:




坑三:为什么非要用reactive?用ref行不行?


我的疑问


既然 ref 也能创建响应式数据,为什么非要推荐用 reactive?


原因分析



  1. 对象解构问题:ref 创建的对象,解构后会失去响应性
  2. 深层响应性:reactive 对深层对象的响应性处理更好
  3. 性能考虑:对于复杂对象,reactive 性能更优

实际对比


// ❌ ref方式 - 可能有问题
const localEditData = ref({...props.editData})
// 如果props.editData是复杂对象,可能响应性不完整

// ✅ reactive方式 - 更稳定
const localEditData = reactive({
id: '',
name: '',
place: ''
})
// 明确初始化所有字段,响应性更可靠

坑四:为什么必须要监听?不用监听行不行?


我的疑问


既然父组件传递了数据,子组件为什么还要监听变化?


原因分析


Vue3的响应式系统特点:



  1. props变化不会自动同步:父组件传递新数据时,子组件的本地副本不会自动更新
  2. 弹窗复用问题:同一个弹窗组件可能编辑不同的数据
  3. 响应式更新:确保子组件能及时响应父组件的数据变化

实际场景


// 父组件中
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 值传递:



  1. editItem.value = data 是引用传递,如果 data 是响应式对象,可能会有意外的副作用
  2. 如果 data 后续被修改,editItem 也会跟着变化
  3. Vue的响应式系统可能无法正确检测到这种变化

解决方案


使用展开运算符创建新对象:


const handleEdit = (data) => {
editItem.value = {...data} // 创建新对象,确保响应性
editRef.value.editDialogVisible = true
}

为什么这样做?



  1. 避免引用问题:创建新对象,避免意外的引用修改
  2. 确保响应性:新对象会触发Vue的响应式更新
  3. 数据隔离:子组件的修改不会影响原始数据

完整解决方案


经过以上踩坑,我最终实现的完整代码如下:


父组件vue



<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>


<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>


子组件


<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>


<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的父子组件通信看似简单,但实际开发中会遇到各种细节问题:



  1. 作用域插槽必须声明参数:


    1. 表单编辑需要本地副本:用 reactive 创建本地数据


    1. props变化需要监听:用 watch 同步数据变化


    1. 避免深度监听:手动同步字段,提升性能


    1. 使用展开运算符:确保响应性和数据隔离

    这些坑虽然让人头疼,但踩过之后对Vue3.0的理解会更深入。希望我的踩坑经历能帮到正在Vue3.0路上奋斗的小伙伴们!

    记住:在Vue3.0的世界里,细节决定成败! 🎯*


    作者:雲墨款哥
    来源:juejin.cn/post/7522367598815576073

0 个评论

要回复文章请先登录注册