vue3父组件和子组件之间传递数据
vue3中父子组件之间如何传递数据这个话题已经被讨论了无数遍,最近在写一个涉及到父子组件间传递数据的程序,理解又深刻了一点,记录下来以备将来查询。
组件的内容
比如说一个外部库mylib.ts是这样写的
export const enum EDIT_MODE {EDIT = 1,VIEW = 2,
}export interface DataType {name: string,comment: string,
}
一个基于element-plus的子组件这样写
<template><el-dialog v-model="visibleRef"><el-input v-model="dataRef.name" v-bind:disabled="editableRef" /><el-input v-model="dataRef.comment" v-bind:disabled="uneditableRef" type="textarea" /><template v-slot:footer><el-button type="primary" v-bind:disabled="uneditableRef" @click="Sumit()"> 确认 </el-button><el-button @click="HandleCancle">取消</el-button></template></el-dialog>
<template>
<script setup lang="ts">
import { EDIT_MODE, DataType } from "./mylib";
import { Ref, ref, Reactive, reactive, PropType, watch } from "vue";const props = defineProps({editModeRef: {type: Number as PropType<EDIT_MODE>,required: true,},data: {type: Object as PropType<DataType>,required: true,},
});const emit = defineEmits<{(event: "Sumit", val: DataType): void;(event: "Cancle"): void;
}>();let visibleRef: Ref<boolean> = ref<boolean>(false);
let uneditableRef: Ref<boolean> = ref<boolean>(false);let dataRef: Reactive<DataType> = reactive<DataType>({name: props.data.name,comment: props.data.comment,
});function Sumit(): void {emit("Sumit", dataRef);
}function HandleCancle(): void {// 虽然visibleRef是v-model绑定的,但是visibleRef来自父组件,无法在子组件内修改// 所以只能发送事件,然后在父组件修改emit("Cancle");
}// props.data本来就是一个object,所以前面defineProps的时候,直接用type: Object as PropType<DataType>就可以
// 然后watch的时候直接作为监控目标处理即可
watch(props.data,(newVal: DataType, oldVal: DataType) => {dataRef.name = newVal.name;dataRef.comment = newVal.comment;},{ deep: true }
);// editModeRef是enum,内容是整型数字,所以前面defineProps的时候,要用type: Number as PropType<EDIT_MODE>
// watch的时候,第一个参数是监控目标,这里必须要用匿名函数返回才能正确识别
watch(() => props.editModeRef,(newVal: number, oldVal: number) => {uneditableRef.value = newVal == EDIT_MODE.VIEW;}
);
</script>
<style lang="less" scoped></style>
父组件的内容是这样的
<template>这里还有一些父组件自己的内容<el-button type="primary" v-on:click="ShowDialog"> 显示对话框 </el-button><el-button type="primary" v-on:click="SwitchDialogEdit"> 变更对话框编辑许可 </el-button><CustomDialogv-model="dialogVisibleRef":edit-mode-ref="workFlowTypeStore.mode":data="dataRef"@SumitWorkFlow="SubmitData"@Cancle="DialogCancle"></CustomDialog>
</template>
<script lang="ts" setup>
import { EDIT_MODE, DataType } from "./mylib";
import { Ref, ref, onMounted, Reactive, reactive } from "vue";let dialogVisibleRef: Ref<boolean> = ref<boolean>(false);
let editModeRef: Ref<EDIT_MODE> =ref<EDIT_MODE>(EDIT_MODE.VIEW);// 初始化默认数据
let dataRef: Reactive<DataType> = reactive<DataType>({name: "",comment: "",
});// 子组件emit Submit事件,父组件接收随事件的数据
function SubmitData(inputData:DataType):void{dataRef.name=inputData.name;dataRef.comment=inputData.comment;console.log(dataRef.name);console.log(dataRef.comment);
}function ShowDialog():void{dialogVisibleRef.value = true;
}// 子组件emit Cancle事件,没有携带附加数据
function DialogCancle():void{dialogVisibleRef.value = false;
}function SwitchDialogEdit():void{if (editModeRef.value==EDIT_MODE.VIEW){editModeRef.value=EDIT_MODE.EDIT;}else{editModeRef.value=EDIT_MODE.VIEW;}
}
</script>
数据传递的单向性
vue3推荐的父组件向子组件传递数据的方法是props,并且强调数据传递是单向的,也就是说子组件无法更改父组件的内容。
但其实vue3父组件向子组件传递数据还有一个方法,就是v-model,这个方法和props类似,数据传递是单向的,子组件无法更改父组件的内容。
这就有了一个容易让人迷惑的点,v-model不是双向绑定吗?在同一个组件内v-model的确是双向绑定的,但是如果是父组件向子组件传递,就是单向的。
注意案例代码中子组件内部的visibleRef,它是v-model双向绑定的,但是在子组件内是无法改变这个变量的。如果说在子组件内的取消按钮是这样的
<el-button @click="visibleRef=false">取消</el-button>
你会发现,无论你怎么点击,这个对话框都不会隐藏。这就是因为子组件无法改变父组件通过v-model传递来的visibleRef变量。
如果想要修改这个变量来实现隐藏对话框的功能,只能把取消按钮绑定HandleCancle函数,然后在子组件内通过HandleCancle函数emit一个事件
function HandleCancle(): void {emit("Cancle");
}
在父组件内接收这个事件,并且修改父组件内控制对话框显示的dialogVisibleRef变量才能实现点击取消按钮隐藏对话框的功能。
传递复杂格式的数据
如果只是传递一些string、number之类简单的数据,直接在子组件里
const props = defineProps({simple: {type: Number,required: true,},another: {type: String,required: true,},
});
甚至还可以增加一些validator之类。
但是如果要传递enum或者更加复杂的数据,就没这简单了。
enum数据的传递
首先要强调一点:enum不是JavaScript自带的数据类型,而是TypeScript引入的类型。而TypeScript实际上会把enum转化为更基础的类型,比如number或者string,这取决于enum类型定义的时候所赋的值,比如说,前面的代码里赋值为1和2,那就会转化为number
根据这个赋值的类型,在子组件里要这样
import { EDIT_MODE } from "./mylib";
import { PropType } from "vue";
const props = defineProps({editModeRef: {type: Number as PropType<EDIT_MODE>,required: true,},
});
比如说,例子中的代码EDIT_MODE这个枚举类型其实是数字,那就把editModeRef的类型设定为Number as PropType<EDIT_MODE>,PropType是vue中的定义。
对象数据的传递
如果是传递更复杂的object类型,那么要这么处理
import { DataType } from "./mylib";
import { PropType } from "vue";const props = defineProps({data: {type: Object as PropType<DataType>,required: true,},
});
因为DataType在TypeScript中最终会被编译为object,所以要用Object as PropType<DataType>来定义传递数据的类型
传递数据的监控
采用了特殊的数据传递方式,其配套的监控方式也要做出调整
enum变量的监控
因为enum数据的类型从enum变为了number,所以watch的时候必须要做点处理,watch的第一个参数——监控目标——必须要用匿名函数返回对应的变量。
import { EDIT_MODE} from "./mylib";
import { PropType, watch } from "vue";const props = defineProps({editModeRef: {type: Number as PropType<EDIT_MODE>,required: true,},
});// editModeRef是enum,内容是整型数字,所以前面defineProps的时候,要用type: Number as PropType<EDIT_MODE>
// watch的时候,第一个参数是监控目标,这里必须要用匿名函数返回才能正确识别
watch(() => props.editModeRef,(newVal: number, oldVal: number) => {uneditableRef.value = newVal == EDIT_MODE.VIEW;}
);
对象变量的监控
因为DataType本质上是object的子类型,所以在defineProps的时候,其实最终TypeScript代码也会被编译为object,也就是说其实没变。watch监控的时候就可以直接写,不需要使用匿名函数返回。
import { DataType } from "./mylib";
import { PropType, watch } from "vue";const props = defineProps({data: {type: Object as PropType<DataType>,required: true,},
});// props.data本来就是一个object,所以前面defineProps的时候,直接用type: Object as PropType<DataType>就可以
// 然后watch的时候直接作为监控目标处理即可
watch(props.data,(newVal: DataType, oldVal: DataType) => {dataRef.name = newVal.name;dataRef.comment = newVal.comment;},{ deep: true }
);
前面提到了v-model传递的变量能不能这样处理:子组件中创建一个内部变量,然后监控传递进来的变量,然后在对话框绑定的是内部变量,“取消”按钮通过调整内部变量来处理对话框的可见与否。
这是不可以的。
el-dialog是否可见是用v-model绑定的,而在父组件嵌入子组件的时候,就是通过v-model绑定的,所以无法通过子组件的内部变量来操纵其已经绑定的来自于父组件的v-model。
不论是v-model还是props从父组件传递到子组件,都是不能在子组件更改的。子组件若是想要更改,就只能通过emit事件来让父组件来更改。