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

Posted

技术标签:

【中文标题】Vue 2 从嵌套组件更新数组中对象的属性【英文标题】:Vue 2 Updating Properties on Object in Array from Nested Component 【发布时间】:2022-01-02 09:09:24 【问题描述】:

我从事 Vue 2 项目已经有一段时间了,在升级我们的 linting 要求后,我发现我们的许多子组件中都有 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,但是,这似乎要么导致代码混乱,要么让人感到不安我们只是以迂回的方式抑制掉毛错误。


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

Component1.vue

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

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


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

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

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

【问题讨论】:

【参考方案1】:

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

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

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

所以你正在处理的实际上只是一个 linting 规则 (vue/no-mutating-props)。有一个正在进行的issue/discussion 提出了一个配置选项,该选项应该允许通过许多好的论据来缓解规则的严格性,但它很少受到维护者的关注(也请随意提高你的声音)

现在你可以做的是:

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

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

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

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

Store 非常适合共享全局状态,但使用只是为了克服 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 或事件总线,但这些非常糟糕

【讨论】:

以上是关于Vue 2 从嵌套组件更新数组中对象的属性的主要内容,如果未能解决你的问题,请参考以下文章

vue.js - 使用原始 json 中的嵌套数组时,递归组件不会更新

Vue 源码之 updateStyle

vue 数据对象内部属性变化 dom无法更新视图问题(elementui tree组件修改节点数据无法更新视图的问题)

VUE从入门到入坑—06. 父子组件通信 / 几种常用的第三方组件库

vue props原理

vue对象或者数组中数据变化但是视图没有更新