Vue 中的 Computed Model

May 12 · 10min

写这篇博文的起因是我最近在刷蓝桥杯国赛的题,有这样一道题目:

题解

题目

这道题的其中一部分需要 在父子组件间双向绑定一个属性,但这个属性不像通常的 v-model 那样在父子组件中值保持完全一样,而是保持一个计算关系(类似于computed)。也就是说:

  • 父组件绑定的值变化后,子组件的值会随之根据计算关系变化;
  • 子组件的值变化后,触发回调事件,父组件的值也会随之根据计算关系变化。

在继续阅读之前,我推荐你读一下 Vue3 的官方文档:


父组件

父组件 App.vue

<script setup>
import { ref } from 'vue'
const number = ref(1)
</script>

<template>
  <div>父组件</div>
  <input v-model="number" style="margin-bottom: 10px">
  <div>子组件 使用`computed`</div>
  <CompComputed v-model="number" />
  <div>子组件 使用`watch`</div>
  <CompWatch v-model="number" />
  <div>子组件 使用`defineModel`</div>
  <CompModel v-model="number" />
</template>

在父组件中,我们定义了number响应式变量,并使用v-model绑定到各个子组件上。稍后我们就要实现三种不同的子组件。


子组件实现一:computed

Vue 3 中 v-model 默认会被编译为modelValue(props)和update:modelValue(emit),因此我们可以使用definePropsdefineEmits来声明我们组件中被传入的属性和事件。

接着我们需要定义一个可读可写的计算属性model。在获取model的值时,应当返回modelValue的值乘以2;在model的值改变时,应当将新值除以2,并触发update:modelValue事件将这个值交给父组件。

通常computed只读的,但我们可以像如下写法一样,给computed传入getset两个方法,使之变成可写的计算属性:

<script setup>
import { computed } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const model = computed({
  get: () => {
    return props.modelValue * 2
  },
  set: (val) => {
    const newVal = val / 2
    emit('update:modelValue', newVal)
    return newVal
  }
})
</script>

<template>
  <input v-model="model">
</template>

子组件实现二:watch

事实上computed应该是一个纯函数的计算操作,而我们在其set方法中产生了副作用(即触发了emit事件),这违反了computed的纯函数特性。因此,我们可以使用watch来代替computed,实现同样的效果:

<script setup>
import { ref, watch } from 'vue'

const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const model = ref(props.modelValue * 2)

watch(() => props.modelValue, (newValue) => {
  model.value = newValue * 2
})

watch(model, (newValue) => {
  emit('update:modelValue', newValue / 2)
})
</script>

<template>
  <input v-model="model">
</template>

这更加好理解,监听props.modelValue的变化,将新值乘以2赋值给model;监听model的变化,将新值除以2赋值给props.modelValue

不必担心,这样写不会导致死循环,因为watch的回调仅在值真正改变时才会触发。

子组件实现三:defineModel

手写emit('update:modelValue', newValue)怎么看都不像是最优雅的写法,实际上 Vue3.4+ 提供了一个便利的宏defineModel,可以更方便地实现双向绑定:

const model = defineModel()

这会创建一个响应式变量model,它接受来自父组件v-model的值并随之动态变化。同时在子组件内监听其变化,当model的值改变时,会自动触发emit('update:modelValue', newValue)

但是这么写只能保证父子组件中双向绑定的值同步(即一模一样),如果要实现计算属性呢?

Vue3 的 官方文档 中提到,defineModel可以携带一些options,其中就包括transformers,可以在读取或同步回父级时转换该值,这正是我们需要的🎉!

可以这样写:

<script setup>
const model = defineModel({
  get: value => value * 2,
  set: value => value / 2
})
</script>

<template>
  <input v-model="model">
</template>

你会发现代码变得相当简洁!Vue 给我们提供了太多便利的写法,满满的 DX!

不过这样写貌似还不太流行,我在 GitHub 上搜了很久才看到有人这样写,希望 Vue 3 的文档能把这种写法的用途强调一下!

你可以在 Vue SFC Playground 中获取全部代码。


>