- A+
这里给大家分享我在网上总结出来的一些知识,希望对大家有所帮助
如何避免写出屎山,优雅的封装组件,在面试官面前大大加分,从这篇文章开始!
保持单向数据流
大家都知道vue是单项数据流的,子组件不能直接修改父组件传过来的props,但是在我们封装组件使用v-model时,不小心就会打破单行数据流的规则,例如下面这样:
<!-- 父组件 --> <my-component v-model="msg"></my-component> <!-- 子组件 --> <template> <div> <el-input v-model="msg"></el-input> </div> </template> <script setup> defineOptions({ name: "my-component", }); const props = defineProps({ msg: { type: String, default: "", }, }); </script>
v-model实现原理
直接在子组件上修改props的值,就打破了单向数据流,那我们该怎么做呢,先看下v-model的实现原理:
<!-- 父组件 --> <template> <my-component v-model="msg"></my-component> <!-- 等同于 --> <my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component> </template>
emit通知父组件修改prop值
所以,我们可以通过emit,子组件的值变化了,不是直接修改props,而是通知父组件去修改该值!
子组件值修改,触发父组件的update:modelValue事件,并将新的值传过去,父组件将msg更新为新的值,代码如下:
<!-- 父组件 --> <template> <my-component v-model="msg"></my-component> <!-- 等同于 --> <my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component> </template> <script setup> import { ref } from 'vue' const msg = ref('hello') </script> <!-- 子组件 --> <template> <el-input :modelValue="modelValue" @update:modelValue="handleValueChange"></el-input> </template> <script setup> const props = defineProps({ modelValue: { type: String, default: '', } }); const emit = defineEmits(['update:modelValue']); const handleValueChange = (value) => { // 子组件值修改,触发父组件的update:modelValue事件,并将新的值传过去,父组件将msg更新为新的值 emit('update:modelValue', value) } </script>
这也是大多数开发者封装组件修改值的方法,其实还有另一种方案,就是利用计算数据的
get、set
computed 拦截prop
大多数同学使用计算属性,都是用get
,或许有部分同学甚至不知道计算属性还有set
,下面我们看下实现方式吧:
<!-- 父组件 --> <script setup> import myComponent from "./components/MyComponent.vue"; import { ref } from "vue"; const msg = ref('hello') </script> <template> <div> <my-component v-model="msg"></my-component> </div> </template> <!-- 子组件 --> <template> <el-input v-model="msg"></el-input> </template> <script setup> import { computed } from "vue"; const props = defineProps({ modelValue: { type: String, default: "", }, }); const emit = defineEmits(["update:modelValue"]); const msg = computed({ // getter get() { return props.modelValue }, // setter set(newValue) { emit('update:modelValue',newValue) }, }); </script>
v-model绑定对象
那么当v-model绑定的是对象呢?
可以像下面这样,computed拦截多个值
<!-- 父组件 --> <script setup> import myComponent from "./components/MyComponent.vue"; import { ref } from "vue"; const form = ref({ name:'张三', age:18, sex:'man' }) </script> <template> <div> <my-component v-model="form"></my-component> </div> </template> <!-- 子组件 --> <template> <div> <el-input v-model="name"></el-input> <el-input v-model="age"></el-input> <el-input v-model="sex"></el-input> </div> </template> <script setup> import { computed } from "vue"; const props = defineProps({ modelValue: { type: Object, default: () => {}, }, }); const emit = defineEmits(["update:modelValue"]); const name = computed({ // getter get() { return props.modelValue.name; }, // setter set(newValue) { emit("update:modelValue", { ...props.modelValue, name: newValue, }); }, }); const age = computed({ get() { return props.modelValue.age; }, set(newValue) { emit("update:modelValue", { ...props.modelValue, age: newValue, }); }, }); const sex = computed({ get() { return props.modelValue.sex; }, set(newValue) { emit("update:modelValue", { ...props.modelValue, sex: newValue, }); }, }); </script>
这样是可以实现我们的需求,但是一个个手动拦截v-model对象的属性值,太过于麻烦,假如有10个输入,我们就需要拦截10次,所以我们需要将拦截整合起来!
监听整个对象
<!-- 父组件 --> <script setup> import myComponent from "./components/MyComponent.vue"; import { ref } from "vue"; const form = ref({ name:'张三', age:18, sex:'man' }) </script> <template> <div> <my-component v-model="form"></my-component> </div> </template> <!-- 子组件 --> <template> <div> <el-input v-model="form.name"></el-input> <el-input v-model="form.age"></el-input> <el-input v-model="form.sex"></el-input> </div> </template> <script setup> import { computed } from "vue"; const props = defineProps({ modelValue: { type: Object, default: () => {}, }, }); const emit = defineEmits(["update:modelValue"]); const form = computed({ get() { return props.modelValue; }, set(newValue) { alert(123) emit("update:modelValue", newValue); }, }); </script>
这样看起来很完美,但是,我们在set中alert(123),它却并未执行!!
原因是:form.xxx = xxx时,并不会触发computed的set,只有form = xxx时,才会触发set
Proxy代理对象
那么,我们需要想一个办法,在form的属性修改时,也能emit("update:modelValue", newValue);
,为了解决这个问题,我们可以通过Proxy代理
<!-- 父组件 --> <script setup> import myComponent from "./components/MyComponent.vue"; import { ref, watch } from "vue"; const form = ref({ name: "张三", age: 18, sex: "man", }); watch(form, (newValue) => { console.log(newValue); }); </script> <template> <div> <my-component v-model="form"></my-component> </div> </template> <!-- 子组件 --> <template> <div> <el-input v-model="form.name"></el-input> <el-input v-model="form.age"></el-input> <el-input v-model="form.sex"></el-input> </div> </template> <script setup> import { computed } from "vue"; const props = defineProps({ modelValue: { type: Object, default: () => {}, }, }); const emit = defineEmits(["update:modelValue"]); const form = computed({ get() { return new Proxy(props.modelValue, { get(target, key) { return Reflect.get(target, key); }, set(target, key, value,receiver) { emit("update:modelValue", { ...target, [key]: value, }); return true; }, }); }, set(newValue) { emit("update:modelValue", newValue); }, }); </script>
这样,我们就通过了
Proxy + computed
完美拦截了v-model
的对象!
然后,为了后面使用方便,我们直接将其封装成hook
// useVModel.js import { computed } from "vue"; export default function useVModle(props, propName, emit) { return computed({ get() { return new Proxy(props[propName], { get(target, key) { return Reflect.get(target, key) }, set(target, key, newValue) { emit('update:' + propName, { ...target, [key]: newValue }) return true } }) }, set(value) { emit('update:' + propName, value) } }) }
<!-- 子组件使用 --> <template> <div> <el-input v-model="form.name"></el-input> <el-input v-model="form.age"></el-input> <el-input v-model="form.sex"></el-input> </div> </template> <script setup> import useVModel from "../hooks/useVModel"; const props = defineProps({ modelValue: { type: Object, default: () => {}, }, }); const emit = defineEmits(["update:modelValue"]); const form = useVModel(props, "modelValue", emit); </script>