保持單向數據流#
大家都知道 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 事件,並將新的值傳過去,父組件將 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", 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>
原文地址: https://juejin.cn/post/7277089907974422588
此文由 Mix Space 同步更新至 xLog
原始鏈接為 https://liu-wb.com/posts/default/computed-intercepts-v-model