Computed

保持单向数据流

大家都知道 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>

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

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>
相关推荐
程序员小八7771 小时前
后端开发初学TypeScript
前端·javascript·typescript
单线程121382 小时前
从案例分析 Vue3 Tokenizer 源码
前端·javascript·vue.js
GIS数据转换器3 小时前
智慧林草“一张图“平台
java·大数据·服务器·前端·javascript·数据库·人工智能
烂蜻蜓3 小时前
Flask入门教程(十):静态文件——CSS、JS与图片的正确管理方式
javascript·css·flask
Mr数据杨4 小时前
【Codex】用学生支持周报模块跟踪个性化支持进展
android·java·javascript·django·codex·项目开发
独立开发之道4 小时前
【three.js教程】Three.js 材质详解:从“不反光“到“物理级真实“怎么选
开发语言·javascript·材质
waillyer5 小时前
企业知识库智能问答 Agent(React + Nest)
javascript·redis·agent
Dovis(誓平步青云)7 小时前
模拟器横评:电脑上看小说、追短剧用什么模拟器?MuMu、雷电、腾讯手游助手实测
android·java·服务器·前端·javascript·电脑
丁婷婷7 小时前
Reflect 在 Vue3 响应式中的作用是什么?
vue.js
xiaominlaopodaren7 小时前
three.js最小地图运行时(十):接入离线逻辑瓦片剖面
javascript·gis·three.js