Maintain Unidirectional Data Flow#
Everyone knows that Vue has a unidirectional data flow, where child components cannot directly modify the props passed from parent components. However, when we encapsulate components using v-model, we might inadvertently break the rules of unidirectional data flow, as shown below:
<!-- Parent Component -->
<my-component v-model="msg"></my-component>
<!-- Child 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 Implementation Principle#
Directly modifying the value of props in the child component breaks the unidirectional data flow. So what should we do? Let's first look at the implementation principle of v-model:
<!-- Parent Component -->
<template>
<my-component v-model="msg"></my-component>
<!-- Equivalent to -->
<my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component>
</template>
Emit Notification to Parent Component to Modify Prop Value#
Therefore, we can use emit. When the value of the child component changes, it does not directly modify the props but notifies the parent component to modify that value!
When the child component's value changes, it triggers the parent's update event and passes the new value. The parent component updates msg to the new value, as shown in the code below:
<!-- Parent Component -->
<template>
<my-component v-model="msg"></my-component>
<!-- Equivalent to -->
<my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component>
</template>
<script setup>
import { ref } from 'vue'
const msg = ref('hello')
</script>
<!-- Child Component -->
<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) => {
// When the child component's value changes, it triggers the parent's update:modelValue event and passes the new value. The parent component updates msg to the new value
emit('update:modelValue', value)
}
</script>
This is also the method most developers use to encapsulate components to modify values. In fact, there is another solution, which is to use computed properties' get and set.
Computed Intercepting Prop#
Most students use computed properties with get, and some may not even know that computed properties also have set. Let's look at the implementation method:
<!-- Parent Component -->
<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>
<!-- Child Component -->
<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 Binding Objects#
What if v-model is binding to an object?
It can intercept multiple values like this:
<!-- Parent Component -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref } from "vue";
const form = ref({
name: 'Zhang San',
age: 18,
sex: 'man'
})
</script>
<template>
<div>
<my-component v-model="form"></my-component>
</div>
</template>
<!-- Child Component -->
<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>
This can meet our needs, but manually intercepting each property value of the v-model object is too cumbersome. If there are 10 inputs, we would need to intercept 10 times, so we need to consolidate the interceptions!
Listening to the Entire Object#
<!-- Parent Component -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref } from "vue";
const form = ref({
name: 'Zhang San',
age: 18,
sex: 'man'
})
</script>
<template>
<div>
<my-component v-model="form"></my-component>
</div>
</template>
<!-- Child Component -->
<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>
This looks perfect, but when we alert(123) in the set, it does not execute!!
The reason is: form.xxx = xxx does not trigger the computed set; only form = xxx will trigger the set.
Proxy Proxy Object#
So, we need to find a way to emit("update", newValue); when the properties of form are modified. To solve this problem, we can use Proxy.
<!-- Parent Component -->
<script setup>
import myComponent from "./components/MyComponent.vue";
import { ref, watch } from "vue";
const form = ref({
name: "Zhang San",
age: 18,
sex: "man",
});
watch(form, (newValue) => {
console.log(newValue);
});
</script>
<template>
<div>
<my-component v-model="form"></my-component>
</div>
</template>
<!-- Child Component -->
<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>
Thus, we have perfectly intercepted the v-model object using Proxy + computed!
Then, for convenience in future use, we directly encapsulate it into a hook.
// useVModel.js
import { computed } from "vue";
export default function useVModel(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)
}
})
}
<!-- Child Component Usage -->
<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>
Original article link: https://juejin.cn/post/7277089907974422588
This article is synchronized and updated to xLog by Mix Space. The original link is https://liu-wb.com/posts/default/computed-intercepts-v-model