Vue3学习踩坑 - 关于自定义组件使用对象的一些思考

647 阅读4分钟

【Vue3】- 关于自定义组件使用对象的一些思考

前言

在前面我们学习了了关于怎么定义v-model(Vue3学习踩坑 - 自定义v-model),在这个过程中,我们使用的都是一些简单的类型,比如string或number类型的,那对于自定义组件,使用对象类型的这种,如果使用了v-model,或者直接定义对象传入子组件一般要怎么处理呢,针对这个问题我使用了一般v-model相同的处理方式,做了些尝试,但是却产生了一些问题,查阅了很多的资料后得到了我认为目前比较正确的结论,特此这里做下记录。

正文

首先我们来看错误的使用方式,同样的我们直接先上demo

vmode1.vue(写法1:通过computed计算属性)

<template>
  <div class="vmodel1">
    <div>
      <button @click="add">add</button>
      <div>{{ user.username }}</div>
      <div>{{ user.age }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import type { iUser } from "@/components/vmodel/user";
interface iProps {
  user: iUser;
}

const props = defineProps<iProps>();

interface iEmits {
  (e: "update:user", user: iUser): void;
}
const emits = defineEmits<iEmits>();

let user = computed({
  get: () => props.user,
  set: (val) => {
    console.log("val", val);
    emits("update:user", val);
  },
});

const add = () => {
  user.value.age++;
};
</script>

vmode1.vue(写法2:通过toRefs将props变成响应式)

<template>
  <div class="vmodel1">
    <div>
      <button @click="add">add</button>
      <div>{{ user.username }}</div>
      <div>{{ user.age }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { toRefs } from "vue";
import type { iUser } from "@/components/vmodel/user";
interface iProps {
  user: iUser;
}

const props = defineProps<iProps>();

let { user } = toRefs(props);

const add = () => {
  user.value.age++;
};
</script>

App.vue

<template>
  <div>
    <vmodel1 v-model:user="user" />
    <div>user:{{ user }}</div>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
import Vmodel1 from "@/components/vmodel/vmodel1.vue";

let user = ref({
  username: "legion",
  age: 20,
});
</script>

click1.gif

在图中我们可以看到,通过这种方式,我们在点击后更新App.vue组件中的user对象的值,从而实现数据的改变。

**但是!**这里如果仔细的看下就不难发现,在vmodel1.vue的第28行的console根本就没有打印!(甚至写法2中,直接就没有定义emit了,但是也实现了改变父组件的数据!),有兴趣的同学可以自己尝试下,在App.vue中,不使用<vmodel1 v-model:user="user" />,而是直接:<vmodel1 :user="user" />,同样可以修改父组件的值,也就是说完全被v-model给迷惑了,其实根本就没有触发事件。

也就是说,我们虽然实现了子组件修改值后父组件的值触发修改,但是实际上根本不是通过emit来实现的,也就是说通过这种方式,只是实现了值的改变,但是却完全违反了单向数据流原则,即原则上,我们不能在子组件中改变父组件的值,否则将会导致数据流将很容易变得混乱而难以理解,因此这里一定要注意这种是错误的示范!!

那么针对这种对象类型的v-model,我们要怎么处理呢?正确的方式应该如下:

这里同样存在两种方式:

方式一:

针对对象的属性进行“拆分”,将需要用到的变量拆分到各个新的计算属性中,比如:

vmodel1.vue

<template>
  <div class="vmodel1">
    <div>
      <button @click="add">add</button>
      <div>{{ user.username }}</div>
      <div>{{ user.age }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { computed } from "vue";
import type { iUser } from "@/components/vmodel/user";
interface iProps {
  user: iUser;
}

const props = defineProps<iProps>();

interface iEmits {
  (e: "update:user", user: iUser): void;
}
const emits = defineEmits<iEmits>();

let age = computed({
  get: () => props.user.age,
  set: (age) => emits("update:user", { ...props.user, age }),
});
const add = () => {
  age.value++;
};
</script>

这里我们将age 字段单拆出来,通过计算属性的getter/setter方法来做取、设置值的动作,当设置计算属性的值的时候,会触发set方法,我们再将新的值emit出去,通知父组件进行处理,这里需要注意的是,我们emit的是要一个完整的对象,包含父组件属性的所有字段,否则父组件对象会被改变。

方式二:

不难发现,对于方式一的这种,比较适合从对象中取值时对象的属性不多的这种情况,但是如果需要用到的字段较多呢?比如说我们有100个字段,总不能把100个都定义计算属性吧?因此我们可以使用下面这种方式:

通过watch函数,来监听对象的值的变化:

<template>
  <div class="vmodel1">
    <div>
      <button @click="add">add</button>
      <div>{{ user.username }}</div>
      <div>{{ user.age }}</div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { reactive, watch } from "vue";
import type { iUser } from "@/components/vmodel/user";
interface iProps {
  user: iUser;
}

const props = defineProps<iProps>();

interface iEmits {
  (e: "update:user", user: iUser): void;
}
const emits = defineEmits<iEmits>();

const user = reactive({ ...props.user });
watch(user, (user) => {
  console.log("user", user);
  emits("update:user", { ...user });
});

const add = () => {
  user.age++;
};
</script>

这里我们先试用reactive,将props的对象转换为一个响应式对象,然后再使用watch来对其进行监听,当值发生改变后,再通过emit将其通知给父组件,同样的这里要传递整个新的对象。

click2.gif

通过上面的两种方式,我们就可以很愉快的在自定义组件中使用对象或者v-model中使用对象了(可以尝试将App.vue中的v-mode:user,改成:user,可以发现是没法更新父组件的值的)。

总结

针对上面的正确传递对象的方式,我们做下总结

  1. 子组件使用计算属性,不要返回父组件的代理对象,否则很容易就会打破单向数据流向的原则
  2. 计算属性的setter方法,需要emit整个对象给父组件,且这个对象,不能是一个响应式类型
  3. 父组件传入的对象,不能使用reactive进行包裹,原因也很简单,因为我们emit的时候,是整个对象,如果使用reactive去包裹对象,会导致它失去响应式!