Vue 中 props 是 Object 可以直接修改吗

2023-08-18 14:12:20 浏览数 (2)

好久没有在知乎上看到好问题了,前几天看到一个,把回答同步过来。

https://www.zhihu.com/question/609822540/answer/3099837968

确实是一个很有争议的问题,团队里也经常讨论这个问题,下边分享下我的想法,也不一定是最佳实践。

首先,不要修改 prop 的值肯定是一条比较好的实践,保证数据的流向明确。

官方文档中也有明确指出:https://vuejs.org/guide/components/props.html#one-way-data-flow

One-Way Data Flow

All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent's state, which can make your app's data flow harder to understand.

In addition, every time the parent component is updated, all props in the child component will be refreshed with the latest value. This means you should not attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console:

代码语言:javascript复制
export default {
  props: ['foo'],
  created() {
    // ❌ 警告!prop 是只读的!
    this.foo = 'bar'
  }
}

There are usually two cases where it's tempting to mutate a prop:

1. The prop is used to pass in an initial value; the child component wants to use it as a local data property afterwards. In this case, it's best to define a local data property that uses the prop as its initial value:

代码语言:javascript复制
// Vue3
const props = defineProps(['initialCounter'])

// counter only uses props.initialCounter as the initial value;
// it is disconnected from future prop updates.
const counter = ref(props.initialCounter)

// Vue2
props: ['initialCounter'],
data: function () {
  return {
    counter: this.initialCounter
  }
}

2. The prop is passed in as a raw value that needs to be transformed. In this case, it's best to define a computed property using the prop's value:

代码语言:javascript复制
// Vue3
const props = defineProps(['size'])

// computed property that auto-updates when the prop changes
const normalizedSize = computed(() => props.size.trim().toLowerCase())

// Vue2
props: ['size'],
computed: {
  normalizedSize: function () {
    return this.size.trim().toLowerCase()
  }
}

为了避免修改 prop 的值,可以在 data 中初始化为 prop 的值然后再去使用或者定义 computed 属性拿到 prop 值再去使用。

当然,上边的写法也仅仅对原始值生效,如果 props 定义成一个 Array 或者 Object,如果把 Object 的值直接赋值给 data:

代码语言:javascript复制
props: ['initialCounterObj'],
data: function () {
  return {
    counterObj: this.initialCounterObj
  }
}

当去修改 counterObj 中的值,虽然看起来没有修改 props 的值,但因为 Objeact 传递进来的是引用,修改 counterObj 的值的时候外部的相应的对象也跟着修改了。

针对这种情况,可以将 Object 摊开,变为一个个原始值。

通过 .sync

父组件

代码语言:javascript复制
<child field1.sync="obj.field1" field2.sync="obj.field2"></child>

子组件

代码语言:javascript复制
export default {
  props: ['field1', 'field2'],
  methods: {
      updateField1(newVal){
           this.$emit('update:field1', newVal)
      },
      updateField2(newVal){
           this.$emit('update:field2', newVal)
      }
  }
}

通过 get set

https://stackoverflow.com/questions/59992698/vuejs-best-practices-for-passing-form-data-to-child-and-back-to-parent?rq=4

父组件

代码语言:javascript复制
<child v-model="obj"></child>

子组件

代码语言:javascript复制
export default {
  props: {
    value: {
      type: Object,
      default: () => ({})
    }
  },

  computed: {
    field1: {
      get() { return this.value.field1 },
      set(field1) { this.$emit('input', {...this.value, field1 })}
    },
    field2: {
      get() { return this.value.field1 },
      set(field2) { this.$emit('input', {...this.value, field2 })}
    }
  }
}

另外一种更暴力的写法就是题主讲到的方案三 「不可变对象型」,每次修改前都把整个对象(或数组)克隆一遍,修改新的对象,再通过 emit 事件把新的对象(这里最好也再克隆一下)传出去。

上边的方案都可以保证不去修改 props 的值。

看下官方对于 props 是 Object/Array 的态度:

Mutating Object / Array Props

When objects and arrays are passed as props, while the child component cannot mutate the prop binding, it will be able to mutate the object or array's nested properties. This is because in JavaScript objects and arrays are passed by reference, and it is unreasonably expensive for Vue to prevent such mutations.

The main drawback of such mutations is that it allows the child component to affect parent state in a way that isn't obvious to the parent component, potentially making it more difficult to reason about the data flow in the future. As a best practice, you should avoid such mutations unless the parent and child are tightly coupled by design. In most cases, the child should emit an event to let the parent perform the mutation.

关键句:you should avoid such mutations unless the parent and child are tightly coupled by design.

因此对于表单场景,我认为符合 parent and child are tightly coupled by design ,很多时候由于表单越来越大,一个 Vue 文件会变得巨大,此时想要拆部分表单出来成为一个组件,这种情况下采用题主所说的方案一「直接修改型」我认为是更佳的,不然的话不管采用什么方式保证不修改 props 都会增加很多代码,反而增加了很多理解成本。

更进一步,对于 Object/Array,是否修改 props 取决于当前组件的通用性,如果这个组件专门为了某个父组件使用或者专门服务于某个页面,并且为了不修改 props 会增加很多工作量,这种情况下直接修改 props 我认为是合适的。

但如果这个组件可能用给其他人,此时修改 props ,如果使用方不清楚的话就可能引发问题。

0 人点赞