妙用computed拦截v-model,面试管都夸我细

前言

大家好,我是林三心,用最通俗易懂的话讲最难的知识点是我的座右铭,基础是进阶的前提是我的初心~

如何避免写出屎山,优雅的封装组件,在面试官面前大大加分,从这篇文章开始!

保持单向数据流

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

这也是大多数开发者封装组件修改值的方法,其实还有另一种方案,就是利用计算数据的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: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>

完美,欢迎留言交流哦!

作者:八月十八
链接:https://juejin.cn/post/7277089907974422588
来源:稀土掘金

结语

我是林三心

  • 一个待过小型toG型外包公司、大型外包公司、小公司、潜力型创业公司、大公司的作死型前端选手;
  • 一个偏前端的全干工程师;
  • 一个不正经的掘金作者;
  • 逗比的B站up主;
  • 不帅的小红书博主;
  • 喜欢打铁的篮球菜鸟;
  • 喜欢历史的乏味少年;
  • 喜欢rap的五音不全弱鸡如果你想一起学习前端,一起摸鱼,一起研究简历优化,一起研究面试进步,一起交流历史音乐篮球rap,可以来俺的摸鱼学习群哈哈,点这个,有7000多名前端小伙伴在等着一起学习哦 --> 摸鱼沸点

广州的兄弟可以约饭哦,或者约球~我负责打铁,你负责进球,谢谢~


相关推荐

  • B 站广州研发工作室解散;外媒曝光苹果中国区丑闻;OpenAI 被曝已叫停新大模型项目 | Q资讯
  • AI创业:3个月做20+场景签8家单背后的秘密
  • 前端架构带你封装axios,一次封装终身受益
  • 北京内推 | 百度AIGC团队招聘多模态方向计算机视觉算法工程师/实习生
  • NeurIPS 2023 | 大模型时代自监督预训练的隐性长尾偏见
  • 预训练机器阅读理解模型:对齐生成式预训练与判别式下游场景
  • [开源]企业级在线办公系统,基于实时音视频完成在线视频会议功能
  • 再看大模型之文档智能训练数据生成方法:PublayNet、TableBank、TableGeneration等数据生成方
  • 一起来卷下整洁架构
  • 波士顿动力用ChatGPT训练机器狗;小米推出大模型版澎湃OS系统;智谱推出ChatGLM3新一代大模型丨AIGC大事日报
  • NVIDIA初创企业半程展示收官!七场分站展示、超200人参会、75家企业参与路演
  • All in AI六年后,联想站在了巨头们的中间!
  • 1027.AI日报:2027年全球AI解决方案支出超5000亿美元
  • 马库斯锐评GPT-5!急需新范式,OpenAI并无优势
  • 微软工资体系被曝光:最低年薪30万,上下限差35倍!三分之一员工不满待遇低
  • 清华微软「LLM+推理智能体」超越GPT-4!攻克数理难题,斩获开源界MATH最佳成绩
  • 卷积网络又行了?DeepMind推翻Transformer最强传说,LeCun怒赞
  • 清华系ChatGLM3现场怼脸演示!多模态直逼GPT-4V,国产Code Interpreter来了
  • 如何找到一批愿意为你付费的用户?
  • 互联网人眼中的外包