• Computed


    保持单向数据流

    大家都知道 vue 是单项数据流的,子组件不能直接修改父组件传过来的 props,但是在我们封装组件使用 v-model 时,不小心就会打破单行数据流的规则,例如下面这样:

    1. <!-- 父组件 -->
    2. <my-component v-model="msg"></my-component>
    3. <!-- 子组件 -->
    4. <template>
    5.   <div>
    6.     <el-input  v-model="msg"></el-input>
    7.   </div>
    8. </template>
    9. <script setup>
    10. defineOptions({
    11.   name: "my-component",
    12. });
    13. const props = defineProps({
    14.   msg: {
    15.     typeString,
    16.     default"",
    17.   },
    18. });
    19. </script>

    v-model 实现原理

    直接在子组件上修改 props 的值,就打破了单向数据流,那我们该怎么做呢,先看下 v-model 的实现原理:

    1. <template>
    2.   <my-component v-model="msg">my-component>
    3.   
    4.   <my-component :modelValue="msg" @update:modelValue="msg = $event">my-component>
    5. template>

    emit 通知父组件修改 prop 值

    所以,我们可以通过 emit,子组件的值变化了,不是直接修改 props,而是通知父组件去修改该值!

    子组件值修改,触发父组件的 update:modelValue 事件,并将新的值传过去,父组件将 msg 更新为新的值,代码如下:

    1. <!-- 父组件 -->
    2. <template>
    3.   <my-component v-model="msg"></my-component>
    4.   <!-- 等同于 -->
    5.   <my-component :modelValue="msg" @update:modelValue="msg = $event"></my-component>
    6. </template>
    7. <script setup>
    8. import { ref } from 'vue'
    9. const msg = ref('hello')
    10. </script>
    11. <!-- 子组件 -->
    12. <template>
    13.   <el-input :modelValue="modelValue" @update:modelValue="handleValueChange"></el-input>
    14. </template>
    15. <script setup>
    16. const props = defineProps({
    17.   modelValue: {
    18.     typeString,
    19.     default'',
    20.   }
    21. });
    22. const emit = defineEmits(['update:modelValue']);
    23. const handleValueChange = (value=> {
    24.     // 子组件值修改,触发父组件的update:modelValue事件,并将新的值传过去,父组件将msg更新为新的值
    25.     emit('update:modelValue'value)
    26. }
    27. </script>

    这也是大多数开发者封装组件修改值的方法,其实还有另一种方案,就是利用计算数据的 getset

    computed 拦截 prop

    大多数同学使用计算属性,都是用 get,或许有部分同学甚至不知道计算属性还有 set,下面我们看下实现方式吧:

    1. <!-- 父组件 -->
    2. <script setup>
    3. import myComponent from "./components/MyComponent.vue";
    4. import { ref } from "vue";
    5. const msg = ref('hello')
    6. </script>
    7. <template>
    8.   <div>
    9.     <my-component v-model="msg"></my-component>
    10.   </div>
    11. </template>
    12. <!-- 子组件 -->
    13. <template>
    14.   <el-input v-model="msg"></el-input>
    15. </template>
    16. <script setup>
    17. import { computed } from "vue";
    18. const props = defineProps({
    19.   modelValue: {
    20.     typeString,
    21.     default"",
    22.   },
    23. });
    24. const emit = defineEmits(["update:modelValue"]);
    25. const msg = computed({
    26.   // getter
    27.   get() {
    28.     return props.modelValue
    29.   },
    30.   // setter
    31.   set(newValue) {
    32.     emit('update:modelValue',newValue)
    33.   },
    34. });
    35. </script>

    v-model 绑定对象

    那么当 v-model 绑定的是对象呢?可以像下面这样,computed 拦截多个值

    1. <!-- 父组件 -->
    2. <script setup>
    3. import myComponent from "./components/MyComponent.vue";
    4. import { ref } from "vue";
    5. const form = ref({
    6.   name:'张三',
    7.   age:18,
    8.   sex:'man'
    9. })
    10. </script>
    11. <template>
    12.   <div>
    13.     <my-component v-model="form"></my-component>
    14.   </div>
    15. </template>
    16. <!-- 子组件 -->
    17. <template>
    18.   <div>
    19.     <el-input v-model="name"></el-input>
    20.     <el-input v-model="age"></el-input>
    21.     <el-input v-model="sex"></el-input>
    22.   </div>
    23. </template>
    24. <script setup>
    25. import { computed } from "vue";
    26. const props = defineProps({
    27.   modelValue: {
    28.     typeObject,
    29.     default: () => {},
    30.   },
    31. });
    32. const emit = defineEmits(["update:modelValue"]);
    33. const name = computed({
    34.   // getter
    35.   get() {
    36.     return props.modelValue.name;
    37.   },
    38.   // setter
    39.   set(newValue) {
    40.     emit("update:modelValue", {
    41.       ...props.modelValue,
    42.       name: newValue,
    43.     });
    44.   },
    45. });
    46. const age = computed({
    47.   get() {
    48.     return props.modelValue.age;
    49.   },
    50.   set(newValue) {
    51.     emit("update:modelValue", {
    52.       ...props.modelValue,
    53.       age: newValue,
    54.     });
    55.   },
    56. });
    57. const sex = computed({
    58.   get() {
    59.     return props.modelValue.sex;
    60.   },
    61.   set(newValue) {
    62.     emit("update:modelValue", {
    63.       ...props.modelValue,
    64.       sex: newValue,
    65.     });
    66.   },
    67. });
    68. </script>

    这样是可以实现我们的需求,但是一个个手动拦截 v-model 对象的属性值,太过于麻烦,假如有 10 个输入,我们就需要拦截 10 次,所以我们需要将拦截整合起来!

    监听整个对象

    1. <!-- 父组件 -->
    2. <script setup>
    3. import myComponent from "./components/MyComponent.vue";
    4. import { ref } from "vue";
    5. const form = ref({
    6.   name:'张三',
    7.   age:18,
    8.   sex:'man'
    9. })
    10. </script>
    11. <template>
    12.   <div>
    13.     <my-component v-model="form"></my-component>
    14.   </div>
    15. </template>
    16. <!-- 子组件 -->
    17. <template>
    18.   <div>
    19.     <el-input v-model="form.name"></el-input>
    20.     <el-input v-model="form.age"></el-input>
    21.     <el-input v-model="form.sex"></el-input>
    22.   </div>
    23. </template>
    24. <script setup>
    25. import { computed } from "vue";
    26. const props = defineProps({
    27.   modelValue: {
    28.     typeObject,
    29.     default: () => {},
    30.   },
    31. });
    32. const emit = defineEmits(["update:modelValue"]);
    33. const form = computed({
    34.   get() {
    35.     return props.modelValue;
    36.   },
    37.   set(newValue) {
    38.     alert(123)
    39.     emit("update:modelValue", newValue);
    40.   },
    41. });
    42. </script>

    这样看起来很完美,但是,我们在 set 中 alert(123),它却并未执行!!

    原因是:form.xxx = xxx 时,并不会触发 computed 的 set,只有 form = xxx 时,才会触发 set

    Proxy 代理对象

    那么,我们需要想一个办法,在 form 的属性修改时,也能 emit("update:modelValue", newValue);,为了解决这个问题,我们可以通过 Proxy 代理

    1. <!-- 父组件 -->
    2. <script setup>
    3. import myComponent from "./components/MyComponent.vue";
    4. import { ref, watch } from "vue";
    5. const form = ref({
    6.   name: "张三",
    7.   age: 18,
    8.   sex: "man",
    9. });
    10. watch(form, (newValue) => {
    11.   console.log(newValue);
    12. });
    13. </script>
    14. <template>
    15.   <div>
    16.     <my-component v-model="form"></my-component>
    17.   </div>
    18. </template>
    19. <!-- 子组件 -->
    20. <template>
    21.   <div>
    22.     <el-input v-model="form.name"></el-input>
    23.     <el-input v-model="form.age"></el-input>
    24.     <el-input v-model="form.sex"></el-input>
    25.   </div>
    26. </template>
    27. <script setup>
    28. import { computed } from "vue";
    29. const props = defineProps({
    30.   modelValue: {
    31.     typeObject,
    32.     default: () => {},
    33.   },
    34. });
    35. const emit = defineEmits(["update:modelValue"]);
    36. const form = computed({
    37.   get() {
    38.     return new Proxy(props.modelValue, {
    39.       get(target, key) {
    40.         return Reflect.get(target, key);
    41.       },
    42.       set(target, keyvalue,receiver) {
    43.         emit("update:modelValue", {
    44.           ...target,
    45.           [key]: value,
    46.         });
    47.         return true;
    48.       },
    49.     });
    50.   },
    51.   set(newValue) {
    52.     emit("update:modelValue", newValue);
    53.   },
    54. });
    55. </script>

    这样,我们就通过了 Proxy + computed 完美拦截了 v-model 的对象!

    然后,为了后面使用方便,我们直接将其封装成 hook

    1. // useVModel.js
    2. import { computed } from "vue";
    3. export default function useVModle(props, propName, emit) {
    4.     return computed({
    5.         get() {
    6.             return new Proxy(props[propName], {
    7.                 get(target, key) {
    8.                     return Reflect.get(target, key)
    9.                 },
    10.                 set(target, key, newValue) {
    11.                     emit('update:' + propName, {
    12.                         ...target,
    13.                         [key]: newValue
    14.                     })
    15.                     return true
    16.                 }
    17.             })
    18.         },
    19.         set(value) {
    20.             emit('update:' + propName, value)
    21.         }
    22.     })
    23. }
    1. <!-- 子组件使用 -->
    2. <template>
    3.   <div>
    4.     <el-input v-model="form.name"></el-input>
    5.     <el-input v-model="form.age"></el-input>
    6.     <el-input v-model="form.sex"></el-input>
    7.   </div>
    8. </template>
    9. <script setup>
    10. import useVModel from "../hooks/useVModel";
    11. const props = defineProps({
    12.   modelValue: {
    13.     typeObject,
    14.     default: () => {},
    15.   },
    16. });
    17. const emit = defineEmits(["update:modelValue"]);
    18. const form = useVModel(props, "modelValue", emit);
    19. </script>
  • 相关阅读:
    Python入门自学进阶-Web框架——25、DjangoAdmin项目应用-分页与过滤
    关键字volatile用法
    虹科方案 | 汽车CAN/LIN总线数据采集解决方案
    基于Android studio+SSH的单词记忆(背单词)APP设计
    电力系统的延时潮流 (CPF)的计算【 IEEE-14节点】(Matlab代码实现)
    MyBatis整合A.CTable自动建表
    Docker基本管理
    U4_1:图论之DFS/BFS/TS/Scc
    【洛谷 P1115】最大子段和 题解(贪心算法)
    Kubernetes二进制部署——理论部分
  • 原文地址:https://blog.csdn.net/why_1639946449/article/details/133049746