Vue 2 从嵌套组件更新数组中对象的属性

Vue 2 Updating Properties on Object in Array from Nested Component

我从事 Vue 2 项目已有一段时间,在升级我们的 linting 要求后,我发现我们的许多子组件中存在 prop 突变错误。在我们的项目中,我们将一个单例对象作为 prop 传递给许多组件,并且最初是直接从子组件更新对象。 Vue seems to suggest using the v-bind.sync feature 用于从子组件更新 props(或使用等效的 v-bindv-on)。然而,这并没有解决 prop 从嵌套组件 数组 修改的问题。

以使用 prop 突变的这段(伪)代码为例:

注:假设const sharedObject: { arrayElements: Array<{ isSelected: boolean }> } = ...

Page.vue

<template>
  ...
  <Component1 :input1="sharedObject" />
  ...
</template>

Component1.vue

<template>
  ...
  <template v-for="elem in sharedObject.arrayElements">
    <Component2 :input2="elem" />
  </template>
  ...
</template>

Component2.vue

<template>
  ...
  <q-btn @click="input2.isSelected = !input2.isSelected"></q-btn>
  ...
</template>

从 Vue 2 中的嵌套组件更新 属性 之类的 input2.isSelected 的正确方法是什么? 我想到的所有方法有缺陷。

有缺陷的方法

相信我们想冒泡说input2.isSelected已在Component2中修改为Page.vue,然而,这似乎要么导致代码混乱,要么让人觉得我们只是在以一种迂回的方式抑制 linting 错误。


为了演示“乱码”方法,首先注意Page.vue不知道elemsharedObject.arrayElements中的索引。因此,我们需要从 Component1Page.vue 发送一个对象,其中包含 input2.isSelected 的状态以及 elemsharedObject.arrayElements 中的索引。这很快就会变得混乱。我们的例子怎么样:

Component1.vue

<template>
  ...
  <template v-for="elem in sharedObject.arrayElements">
    <template v-for="elem2 in elem.arrayElements">
       <Component2 :input2="elem2" />
    </template>
  </template>
  ...
</template>

在这种情况下,我们可能需要传递 2 个索引!这对我来说似乎不是一个可持续的解决方案。


我想到的替代方案是一个回调函数(作为 prop 通过组件层次结构传递),它将我们要更新的元素和包含我们要更新的属性的对象作为输入(使用 Object.assign).

这让我非常不安,因为我不知道我们不能从子组件更新传递引用属性的真正原因。对我来说,这似乎只是一种迂回的方式,可以在没有 linter 注意到的情况下更新从 Component2 传入的内容。如果在将 props 传递给子组件时发生了一些神奇的修改,那么肯定会将我在 Component2 中收到的对象传递给回调函数并在父组件中修改它基本上只是更新子组件中的 prop,但更复杂。

在 Vue 2 中解决这个问题的正确方法是什么?

很好的问题和对 Vue 生态系统中这个长期存在问题的现状的分析。

是的,修改来自子 is a problem 的“值类型”道具,因为它会产生运行时问题(父在重新渲染时覆盖更改),因此 Vue 在发生这种情况时会生成运行时错误...

修改作为 prop 传递的对象的 属性 从“代码工作正常”的 POV 是可以的。不幸的是,社区中有一些有影响力的人(以及许多盲目追随他们的人)认为这是一种反模式。我不同意并多次提出我的论点(例如here)。您很好地描述了原因 - 它只会创建不必要的 complexity/boilerplate 代码...

所以你正在处理的实际上只是一个 linting 规则(vue/no-mutating-props). There is an ongoing issue/discussion 提出的配置选项应该允许通过许多好的参数来缓解规则的严格性,但它很少受到关注维护者(也可以在那里提高你的声音)

现在您可以做的是:

  1. 禁用规则(远非完美,但幸运的是,由于 Vue 运行时错误,您可以在开发过程中捕捉到真正的错误情况)
  2. 接受现实并使用变通办法

解决方法 - 使用全局状态(像 Vuex 或 Pinia 这样的存储)

注意:Pinia 是首选,因为下一版本的 Vuex 将具有相同的 API

一般的想法是将 sharedObject 放在商店中并仅使用 props 将子组件导航到正确的对象 - 在您的情况下 Component2 将通过 prop 接收索引并检索使用它的商店中的正确元素。

Stores 非常适合共享全局状态,但使用它只是为了克服 linting 规则是不好的。因此,组件与商店耦合,因此可重用性受到影响,测试也更加困难

解决方法 - 事件

是的,仅使用事件可能会造成混乱和大量样板代码(尤其是当您嵌套组件超过 2 层时),但有一些方法可以使事情变得更清晰。

例如,在您的情况下,Component2 不需要知道索引,因为您可以像这样处理事件

// Component1
<template>
  ...
   <template v-for="elem in sharedObject.arrayElements">
    <template v-for="(elem2, index) in elem.arrayElements">
       <Component2 :input2="elem2" @update="updateElement($event, index)" />
    </template>
  </template>
  ...
</template>

在您的情况下,Component2 仅处理单个布尔值 属性 的变化,因此 $event 可以是简单的布尔值。如果在 Component2 中有多个 属性 被改变, $event 可以是一个对象,你可以使用对象扩展语法来“简化” Component2 (使用一个事件而不是多个事件 - 每个 属性)

// Component2
<template>
  ...
  <input v-model="someText" type="text">
  <q-btn @click="updateInput('isSelected', !input2.isSelected)"></q-btn>
  ...
</template>
<script>
export default {
  props: ['input2'],
  computed: {
    someText: {
      get() { return this.input2.someText },
      set(newVal) { updateInput('someText', newVal) }
    }
  },
  methods: {
    updateInput(propName, newValue) {
      const updated = { ...this.input2 } // make a copy of input2 object
      updated[propName] = newValue  // change selected property

      this.$emit('update', updated) // send updated object to parent
    }
  }
}
</script>

好吧...我更喜欢禁用规则并设置一些明确的命名约定以指示组件负责更改其输入...

请注意,还有其他解决方法,例如使用 this.$parentinject\provide 或事件总线,但这些 非常糟糕