一方向データフローの保持#
皆さんご存知の通り、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 のオブジェクトを完璧にインターセプトしました!
その後、使用を簡単にするために、これをフックとしてラップします。
// 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