這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 如何避免寫出屎山,優雅的封裝組件,在面試官面前大大加分,從這篇文章開始! 保持單向數據流 大家都知道vue是單項數據流的,子組件不能直接修改父組件傳過來的props,但是在我們封裝組件使用v-model時,不小心就會打破單行數據流的規則, ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
如何避免寫出屎山,優雅的封裝組件,在面試官面前大大加分,從這篇文章開始!
保持單向數據流
大家都知道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>