写这篇博文的起因是我最近在刷蓝桥杯国赛的题,有这样一道题目:
这道题的其中一部分需要 在父子组件间双向绑定一个属性,但这个属性不像通常的 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),因此我们可以使用defineProps
和defineEmits
来声明我们组件中被传入的属性和事件。
接着我们需要定义一个可读可写的计算属性model
。在获取model
的值时,应当返回modelValue
的值乘以2;在model
的值改变时,应当将新值除以2,并触发update:modelValue
事件将这个值交给父组件。
通常computed
是只读的,但我们可以像如下写法一样,给computed
传入get
和set
两个方法,使之变成可写的计算属性:
<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 中获取全部代码。