返回

Vue3 v-model 绑定 Prop 报错?三种解决方案详解

vue.js

Vue 3 中 v-model 绑定 Prop 的问题

在使用 Vue 3 开发时,经常会遇到一个错误提示:“v-model cannot be used on a prop, because local prop bindings are not writable”。 这个错误表明你试图直接通过 v-model 修改一个作为 props 传入的父组件数据,这种做法在 Vue 中是不允许的。本篇文章将探讨这一问题的根源,并提供有效的解决方案。

问题分析

Vue 的单向数据流机制要求 props 是只读的,子组件不能直接修改父组件传递下来的数据。 当我们使用 v-model 将 props 数据直接绑定到输入框这类可编辑元素上时,相当于子组件尝试修改 props,这会违反 Vue 的单向数据流原则。报错的目的就是提醒开发者遵循数据流规则,保证应用的状态可预测。

错误的代码展示如下:

<template>
  <input v-model="timer" type="number">
</template>

<script>
export default {
  props: ['timer'],
  setup(props) {
    return {}
  }
}
</script>

上述代码中,timer 是通过 props 传入的数据,又使用 v-model 双向绑定,这样直接修改 props 会触发报错。

解决方案

要解决这个问题,核心在于不直接修改 props,而是通过一种间接的方式更新父组件数据。这里推荐三种解决方案:

方案一: 使用本地响应式数据配合 $emit

此方案是 Vue 官方推荐的方法。创建一个本地的响应式数据来存储当前的值,并且使用 $emit 发射事件通知父组件更新。

  1. 在子组件的 setup 函数中,使用 ref 创建一个本地响应式数据,并使用 prop 的值初始化该数据。
  2. input 事件发生时,使用 $emit 触发一个自定义事件,该事件的名称遵循 update:propName 的命名规范,将新的值作为参数传递给父组件。
  3. 父组件需要监听子组件发射的事件,并修改绑定的值。

子组件代码示例:

<template>
  <input :value="localTimer" @input="updateTimer" type="number">
</template>

<script>
import { ref, onMounted } from 'vue'
export default {
  props: ['timer'],
  setup(props, { emit }) {
    const localTimer = ref(props.timer)

      onMounted(()=>{
          localTimer.value = props.timer
      })
      
    const updateTimer = (event) => {
      localTimer.value = Number(event.target.value)
      emit('update:timer', Number(event.target.value))
    }
    return { localTimer, updateTimer }
  }
}
</script>

父组件代码示例:

<template>
  <AudienceTimerSlide :timer.sync="myTimer"></AudienceTimerSlide>
</template>

<script setup>
  import { ref } from 'vue';
  import AudienceTimerSlide from './components/AudienceTimerSlide.vue';

  const myTimer = ref(100)
</script>

操作步骤:

  • 子组件 AudienceTimerSlide 将 props 中传递的 timer 值用 ref 创建一个 localTimer 进行内部管理。
  • 在 input 发生更改的时候, 使用 update:timer 事件向上通知。
  • 父组件需要监听子组件发射的update:timer事件并更新本地绑定值 myTimer, 为了便捷我们可以使用.sync的语法糖。

原理:

  • 父组件负责管理数据状态,子组件只负责触发修改数据的操作,这样的结构保证数据单向流。

方案二: 使用 computed 计算属性(适合简单场景)

如果只需要在组件内部对 prop 进行简单计算或格式化,并且不需要通知父组件更新原始 prop 数据,可以使用计算属性。计算属性会将 prop 值转化为新的数据,实现间接绑定。

  1. setup 中定义一个计算属性,get 返回 prop 值,set 负责更新响应式变量(这个变量用于 v-model),同时父组件 prop 值不会被修改。

代码示例:

<template>
  <input v-model="computedTimer" type="number">
</template>

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

export default {
  props: ['timer'],
    setup(props, context) {
    const _internalTimer = ref(props.timer);
    onMounted(() => {
      _internalTimer.value = props.timer;
      });
    const computedTimer = computed({
          get: () => _internalTimer.value,
          set: (value) => (_internalTimer.value = Number(value)),
          })
          
        return { computedTimer }
     }
}
</script>

操作步骤:

  • 利用 computed 计算属性拦截了 v-model 直接修改数据的情况,并把它转变成 _internalTimer 的间接修改。
  • 这样做的好处在于, 代码可读性提高, 只适用于简单的场景,如果逻辑复杂还是需要采用方案一。

方案三: 使用 v-model 指令并设置modelModifiers

如果不想书写繁琐的emit方法,我们可以使用 modelModifiers, modelModifiers可以传递额外的信息到子组件。

代码示例:

<template>
  <input :value="localTimer" @input="$emit('update:timer', Number($event.target.value))" type="number"  >
</template>
<script setup>
    import { ref,  onMounted, } from 'vue';

  const props = defineProps({
        timer: {
            type: Number,
            default: 0,
        },
    })
 const localTimer = ref(props.timer)

  onMounted(()=>{
      localTimer.value = props.timer
  })


defineEmits(['update:timer'])

</script>

操作步骤:

  1. 移除子组件上的 v-model 指令。
  2. 使用 @input="$emit('update:timer', Number($event.target.value))" 向上触发事件,
  3. 在父组件使用 .sync 修改 props。

原理:
* 使用此方法相当于自己创建了 v-model的语法糖, 省略了一部分 v-model 底层操作的步骤。 相比方案一这种代码量更少, 而且更接近 vue 的原生习惯。

安全建议

在处理用户输入时,始终要进行必要的验证和处理,避免出现潜在的安全风险。对于数字类型的输入,请确保在提交到服务器之前转换为合适的类型,并检查是否符合范围。 使用 v-model 指令可以提升效率, 但是你需要明白 v-model 底层工作原理, 选择最合适的解决方案以应对不同的场景。

以上就是解决 “v-model cannot be used on a prop” 错误的三种主要方案,开发者可根据实际场景选择最合适的方法。 深刻理解数据流机制和 v-model 指令的用法,将能避免许多常见问题,同时写出高效稳定的应用。