Vue 3.4+ 中 defineModel 该如何正确使用呢?

在官网中查阅到了 defineModel 这个宏 API,于是就自己测试了一下,但是遇到一个很诡异的地方无法理解。

下面是测试代码:

这个是父组件,结构很简单,引入了一个子组件

<script lang="ts" setup>
import { ref, onBeforeUpdate, onUpdated } from "vue";

import Context from "../Context.vue";

const count = ref(100);
</script>
<template>
  <Context v-model="count" />
</template>

子组件如下

<script lang="ts" setup>
import { ref, nextTick } from "vue";
const n = ref(0);

const count = defineModel({ type: Number, default: 13 });

count.value += 1;
count.value += 2;
count.value += 3;
count.value += 4;
 console.log(count.value); // 这里为什么打印是 100?

nextTick(() => {
  console.log(count.value); //这里结果为什么是 104??
});
</script>
<template>
<div> {{count}}</div>
</templage>

问题:子组件的输出是不是有点问题呢?image.png

阅读 4.6k
avatarAI BotBETA

在 Vue 3.4+ 中,defineModel 是一个宏 API,用于定义组件的模型(model),它使得组件可以方便地通过 v-model 绑定到父组件。然而,在你提供的代码中,存在一些误解和错误使用 defineModel 的情况。

首先,defineModel 宏 API 的作用是定义组件的 props 和 emit 事件,以支持 v-model 的语法糖。它并不会自动处理 prop 的默认值或类型校验。对于默认值,你应该在 props 中直接设置,而对于类型校验,Vue 3 提供了 definePropsdefineEmits 宏 API 来分别定义 props 和 emit 事件。

在你的子组件中,你尝试使用 defineModel 来定义一个 count 属性,并且给它指定了类型和默认值。但是,你实际上并没有在 props 中定义 count,而且 defineModel 并不会设置默认值。因此,count 的值将由父组件通过 v-model 绑定传入。

在父组件中,你通过 v-model="count"count 绑定到子组件。这意味着子组件的 count prop 的值将会与父组件的 count ref 同步。因此,当父组件的 count 发生变化时,子组件的 count prop 也会相应地变化。

关于你提到的打印结果:

  1. 在子组件中,你尝试对 count 进行一系列加法操作,但此时 count 的值实际上还没有从父组件传入,所以这些操作是没有意义的。
  2. console.log(count.value); 打印的 100 是因为此时 count 的值已经被父组件的 count ref 同步了。在父组件中,你初始化了 count100,所以子组件中的 count prop 也是 100
  3. nextTick(() => { console.log(count.value); }) 打印的 104 是因为你在 nextTick 的回调函数中打印 count.valuenextTick 会在 DOM 更新后执行回调函数,此时子组件的 count prop 已经被父组件的 count ref 同步,并且由于你在子组件中尝试对 count 进行加法操作(尽管这些操作没有意义),所以 count.value 的值变成了 104

综上所述,你遇到的输出问题主要是由于对 defineModel 的错误使用以及对 Vue 组件 props 和 v-model 的工作原理理解不足导致的。你应该直接使用 propsemit 来处理 v-model 的绑定,而不是使用 defineModel

以下是一个修正后的子组件示例:

<script lang="ts" setup>
import { defineProps, defineEmits, nextTick } from "vue";

const props = defineProps({
  modelValue: {
    type: Number,
    default: 13
  }
});

const emit = defineEmits(['update:modelValue']);

const count = computed({
  get() {
    return props.modelValue;
  },
  set(value) {
    emit('update:modelValue', value);
  }
});

nextTick(() => {
  console.log(count.value); // 这里将会打印从父组件传入的 modelValue 的值
});
</script>

<template>
  <div>{{ count }}</div>
</template>

在这个修正后的示例中,我们使用了 defineProps 来定义 modelValue prop,并使用 defineEmits 来定义 update:modelValue emit 事件。我们还使用了一个计算属性 count 来封装对 modelValue 的访问和更新,以便能够正确地触发 v-model 的更新逻辑。这样,当父组件的 count 发生变化时,子组件的 count prop 也会相应地变化,并且子组件可以通过更新 count 来通知父组件进行更新。

2 个回答
✓ 已被采纳

众所周知,v-model 仅仅是语法糖,父组件中的 <Context v-model="count" />将被编译为:

// 父组件
<Context
  :modelValue="count"
  @update:modelValue="$event => (count = $event)"
/>

那么子组件中的 defineModel 又是什么呢?从上面的代码中可以看出,它应该为子组件定义了一个包含 modelValueprops 和一个自定义事件 update:modelValue。类似于:

// 子组件
const props = defineProps<{ modelValue: number }>(),
  emit = defineEmits<{ 'update:modelValue': [value: number] }>()

同时它还有一个返回值,类型为 ModelRef。它实际上也只是让我们使用更方便而包装的一层代理而已。当我们读取它的值时,它实际上是在访问 props.modelValue;当我们设置它的值时,实际上也只是触发了 update:modelValue 事件而已:

const modelRef = {
  get value() {
    return props.modelValue
  },
  set value(newValue) {
    emit('update:modelValue', newValue)
  }
}

此时我们的应用中有两个 count

// 父组件
// Ref<number>
const count = ref(100)

// 子组件
// ModelRef<number>
const count = defineModel(/** code */)

一个是 Ref,另一个是 ModelRef。看上去似乎相同,实际上没有任何关系。他们之间实际上是通过父子组件间的 props 相联系起来的。

让我们回到问题,当子组件中执行 count.value += 1 时,发生了什么:

count.value += 1

==>

count.value = count.value + 1

==>

emit('update:modelValue', count.value + 1)

==>

emit('update:modelValue', props.modelValue + 1)

当执行完这一语句时,父组件中的 Ref 确实变成了 101,但是子组件的 props 的更新还要等到下一个 tick,所以子组件中的 ModelRef 还是 100。

所以接下来 count.value += 2count.value += 3count.value += 4 将会分别把父组件的 Ref 设置为 102,103,104。

新手上路,请多包涵

如果为 defineModel prop 设置了一个 default 值且父组件没有为该 prop 提供任何值,会导致父组件与子组件之间不同步

撰写回答
你尚未登录,登录后可以
  • 和开发者交流问题的细节
  • 关注并接收问题和回答的更新提醒
  • 参与内容的编辑和改进,让解决方法与时俱进
推荐问题
宣传栏