Vue 3计算属性默认只读,如何通过getter/setter实现可写与自定义组件v-model?

57 阅读13分钟

计算属性的默认「只读」特性

在Vue 3中,计算属性(computed)的核心价值是封装依赖数据的计算逻辑,并自动响应依赖变化。比如我们有firstNamelastName两个原始数据,要显示用户的全名fullName,用计算属性可以这么写:

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

const firstName = ref('John')
const lastName = ref('Doe')

// 计算属性默认只有getter(读取时触发)
const fullName = computed(() => {
  return `${firstName.value} ${lastName.value}`
})
</script>

<template>
  <p>全名:{{ fullName }}</p> <!-- 显示 "John Doe" -->
</template>

这里的fullName本质是一个只读属性——它的逻辑只负责“读取”firstNamelastName并返回结果。如果我们尝试直接修改fullName(比如fullName.value = 'Jane Smith'),Vue会抛出错误:Cannot assign to read only property 'fullName' of object '#<Object>'

为计算属性添加setter:从「只读」到「可写」

那如果我们需要通过修改计算属性,同步更新背后的依赖数据呢?比如用户输入“Jane Smith”时,自动把firstName改成JanelastName改成Smith——这时候就需要给计算属性加一个setter(修改时触发)。

Vue 3允许我们将计算属性定义为一个包含getset的对象

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

const firstName = ref('John')
const lastName = ref('Doe')

const fullName = computed({
  // 读取时触发:返回计算结果
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  // 修改时触发:接收新值,更新依赖数据
  set(newValue) {
    // 将新值拆分为 firstName 和 lastName(比如 "Jane Smith" → ["Jane", "Smith"])
    const [first, last = ''] = newValue.split(' ')
    firstName.value = first
    lastName.value = last
  }
})
</script>

<template>
  <p>全名:{{ fullName }}</p>
  <!-- 修改fullName,会触发setter -->
  <input v-model="fullName" placeholder="输入全名" />
</template>

现在,当我们在输入框中输入“Jane Smith”时:

  1. 输入框修改fullName → 触发setter,接收新值"Jane Smith"
  2. setter将新值拆分为first = "Jane"last = "Smith"
  3. 更新firstNamelastName → 依赖数据变化,触发getter重新计算fullName
  4. 输入框和页面同步显示最新的fullName

双向数据处理:用计算属性封装自定义组件的v-model

在Vue 3中,自定义组件的v-model默认依赖modelValue(props)和update:modelValue(事件)实现双向绑定。而计算属性的getter/setter刚好可以简化这个逻辑——用getter读取props的值,用setter触发更新事件。

往期文章归档
免费好用的热门在线工具

比如我们封装一个带提示的输入组件CustomInput,需要支持v-model双向绑定:

<!-- CustomInput.vue(子组件) -->
<script setup>
import { computed } from 'vue'

// 接收父组件传递的 modelValue(v-model的默认props)
const props = defineProps(['modelValue', 'placeholder'])
// 声明要触发的事件(更新父组件数据)
const emit = defineEmits(['update:modelValue'])

// 用计算属性代理 props 和 emit
const inputValue = computed({
  get() {
    // 从props读取父组件的v-model值
    return props.modelValue
  },
  set(newValue) {
    // 触发事件,通知父组件更新v-model的值
    emit('update:modelValue', newValue)
  }
})
</script>

<template>
  <div class="custom-input">
    <span class="hint">提示:</span>
    <!-- 用v-model绑定计算属性,自动同步props和emit -->
    <input v-model="inputValue" :placeholder="props.placeholder" />
  </div>
</template>

<style scoped>
.custom-input { margin: 10px 0; }
.hint { color: #666; margin-right: 5px; }
</style>

父组件使用时,直接用v-model绑定数据即可:

<!-- Parent.vue(父组件) -->
<script setup>
import { ref } from 'vue'
import CustomInput from './CustomInput.vue'

const message = ref('Hello Vue 3!')
</script>

<template>
  <h3>父组件数据:{{ message }}</h3>
  <!-- 用v-model绑定CustomInput,双向同步 -->
  <CustomInput v-model="message" placeholder="输入内容" />
</template>

这个例子的核心逻辑是:

  • 子组件的inputValue通过getter读取父组件的modelValue(即message);
  • 输入框修改inputValue → 触发setter,调用emit('update:modelValue', newValue)
  • 父组件的message同步更新,再通过props.modelValue传递给子组件,完成双向绑定。

计算属性getter/setter的工作流程(流程图)

graph TD
A[读取计算属性(如{{ fullName }})] --> B[触发getter]
B --> C[返回依赖数据的计算结果(firstName + lastName)]
D[修改计算属性(如输入框改fullName)] --> E[触发setter]
E --> F[更新依赖数据(firstName/lastName)]
F --> G[依赖数据变化,重新触发getter]
G --> H[计算属性值更新,页面同步]

课后Quiz:用计算属性实现自定义下拉组件的v-model

问题:请封装一个CustomSelect组件,支持以下功能:

  1. 接收父组件的options(下拉选项数组,格式如[{ value: 'vue', label: 'Vue 3' }]);
  2. 支持v-model双向绑定父组件的selectedValue
  3. 选中下拉项时,父组件的selectedValue自动更新。

答案解析
核心思路是用计算属性的getter读取props.modelValue,用setter触发update:modelValue事件:

<!-- CustomSelect.vue(子组件) -->
<script setup>
import { computed } from 'vue'

// 接收父组件的 props:modelValue(v-model值)、options(下拉选项)
const props = defineProps(['modelValue', 'options'])
// 声明触发父组件的事件
const emit = defineEmits(['update:modelValue'])

// 计算属性代理 v-model 逻辑
const selected = computed({
  get() {
    return props.modelValue
  },
  set(newValue) {
    emit('update:modelValue', newValue)
  }
})
</script>

<template>
  <select v-model="selected">
    <option
      v-for="opt in options"
      :key="opt.value"
      :value="opt.value"
    >
      {{ opt.label }}
    </option>
  </select>
</template>

父组件使用

<script setup>
import { ref } from 'vue'
import CustomSelect from './CustomSelect.vue'

const selectedValue = ref('vue') // 默认选中Vue 3
const options = ref([
  { value: 'vue', label: 'Vue 3' },
  { value: 'react', label: 'React' },
  { value: 'svelte', label: 'Svelte' }
])
</script>

<template>
  <p>选中框架:{{ selectedValue }}</p>
  <CustomSelect v-model="selectedValue" :options="options" />
</template>

常见报错及解决

1. 报错:Cannot assign to read only property 'fullName' of object '#<Object>'

  • 原因:尝试修改没有setter的计算属性(默认只读)。
  • 解决:给计算属性添加setter函数,或检查是否误修改了只读计算属性。
  • 预防:如果需要修改计算属性,提前设计setter逻辑;避免在模板/方法中直接修改只读计算属性。

2. 报错:Infinite loop in computed property 'fullName'

  • 原因setter中修改的属性是计算属性的依赖,导致gettersetter无限循环。比如setter里修改了fullName本身,或修改的firstName又触发fullNamegetter,形成循环。
  • 解决:确保setter修改的是独立的原始数据(如firstName/lastName),而非计算属性本身;或添加条件判断避免循环:
    set(newValue) {
      const [first, last = ''] = newValue.split(' ')
      // 只有新值与当前值不同时才更新(避免循环)
      if (first !== firstName.value) firstName.value = first
      if (last !== lastName.value) lastName.value = last
    }
    

3. 报错:自定义组件v-model不更新父组件数据

  • 原因setter没有正确触发update:modelValue事件,或事件名拼写错误(Vue 3默认事件名是update:modelValue)。
  • 解决:检查emit的事件名是否正确;确保computedsetter中调用了emit('update:modelValue', newValue)
  • 预防:用defineEmits明确声明事件名,避免拼写错误。

参考链接