父子组件之间的数据双向绑定是Vue.js的一大特色,它让数据的传递变得更加方便。但是其实现方式及原理在Vue2和Vue3中却存在着一定的差异。我们在项目或者面试中或许经常看到.sync修饰符,v-model,以及Vue3新特性defineModel。这些似乎都和数据双向绑定有关,但是非常容易混淆和错用。本文将详细介绍在Vue2和Vue3中这几种方式的区别及使用场景。

1.v-model

(1)Vue2实现

在Vue2中,v-model 实际上 等于 :value + @input
因此在其子组件上需要使用 props 接收名为 value 的参数,并使用 $emit 来触发名为 input 的事件。
由于 value 和 input 的写法固定,故在一个标签上只能使用一个 v-model。

<!-- Parent 组件-->
<template>
  <div id="parent">
    <h1>parent组件中的值:{{ age }}</h1>
    <Child v-model="age"></Child>
    <!-- 等同于: -->
    <!-- <Child :value="age" @input="age = $event"></Child> -->    
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
export default {
  data() {
    return {
      age: 18
    }
  },
  components: {Child}
}
</script>
<!-- Child 组件-->
<template>
    <div class="child">
        <h1>child组件中的值:{{ value }}</h1>
        <button @click="$emit('input',value+1)">点我+1</button>
    </div>
</template>

<script> 
export default { 
    props: {value:Number}
}
</script>

(2)Vue3实现

在Vue3中,v-model后面可以指定传递的参数名,如 v-model:参数名=”参数值”。在不指定参数名时默认为 v-model:modelValue=”参数值”。
v-model:参数名 实际上又等于 :参数名 + @update:参数名
因此在其子组件上需要使用 defineProps 接收名为“参数名”的参数,并使用 defineEmits 来触发名为 update:参数名 的事件。
由于可以自定义参数名,故在一个标签上可以使用多个 v-model,但要注意参数名不要重复。

<!-- Parent 组件-->
<template>
  <div id="app">
    <h1>parent组件中的值:{{ age }}</h1>
    <Child v-model="age"></Child>
    <!-- 等同于: -->
    <!-- <Child v-model:modelValue="age"></Child> -->
    <!-- 等同于: -->
    <!-- <Child :modelValue="age" @update:modelValue="age = $event"></Child> -->
  </div>
</template>
<script setup>
import Child from "@/components/Child.vue";
import { ref } from 'vue';
const age = ref(18)
</script>
<!-- Child 组件-->
<template>
        <h1>child组件中的值:{{ modelValue }}</h1>
        <button @click="emit('update:modelValue', modelValue + 1)">点我+1</button>
</template>
<script setup>
defineProps({modelValue: Number})
const emit = defineEmits()
</script>

2.sync修饰符

.sync修饰符只在Vue2中,Vue3中已不再支持。
:参数名.sync 实际上等于 :参数名 + @update:参数名
因此在其子组件上需要使用 props 接收名为“参数名”的参数,并使用 $emit 来触发名为 update:参数名 的事件。
由于可以自定义参数名,故在一个标签上可以使用多个.sync修饰符来实现多个数据的双向绑定。

<!-- Parent 组件-->
<template>
  <div id="parent">
    <h1>parent组件中的值:{{ age }}</h1>
    <Child :value.sync="age"></Child>
    <!-- 等同于 -->
    <!-- <Child :value="age" @update:value="age = $event"></Child> -->
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
export default {
  data() {
    return {
      age: 18
    }
  },
  components: {Child}
}
</script>
<template>
    <div class="child">
        <h1>child组件中的值:{{ value }}</h1>
        <button @click="$emit('update:value',value+1)">点我+1</button>
    </div>
</template>

<script> 
export default { 
    props: {value:Number}
}
</script>

3.defineModel

defineModel是Vue3.4版本正式推出的新特性。它的出现可以极大的简化父子组件数据双向绑定的步骤,在子组件中将不再需要编写 defineProps 和 defineEmits。

<!-- parent 组件 -->
<template>
    <h1>parent组件中的值:{{ age }}</h1>
    <Child v-model="age"></Child>
    <!-- 等同于 -->
    <!-- <Child v-model:modelValue="age"></Child> -->
    <!-- 等同于 -->
    <!-- <Child :modelValue="age" @update:modelValue="age = $event"></Child> -->
</template>
<script lang="ts" setup>
import Child from "@/components/Child.vue";
import { ref } from 'vue';
const age = ref(18)
</script>
<!-- 子组件 -->
<template>
    <div class="child">
        <h1>child组件中的值:{{ age }}</h1>
        <button @click="age = age + 1">点我+1</button>
    </div>
</template>
<script lang="ts" setup>
const age = defineModel()
// 等同于
// const age = defineModel('modelValue') // 不传入参数即为默认值 modelValue
</script>

总结:在 Vue2 中,v-model 和 .sync修饰符的区别在于前者只能有一个,后者可以有多个;在 Vue3 中,移除了.sync修饰符,该功能可在 v-model 上加一个参数代替,同样可以有多个 v-model,其原理和.sync修饰符基本相同;而defineModel 是3.4版本新出的特性,也支持多个 v-model 进行数据双向绑定,且语法上更为简单。