【问题标题】:How to "Avoid mutating a prop directly" in a recursive component如何在递归组件中“避免直接改变道具”
【发布时间】:2020-03-13 14:51:55
【问题描述】:

我理解这个概念:孩子使用自己的道具数据副本,当它发生更改时,它可以$emit 进行更改,以便父母可以更新自己。

但是,我正在处理递归树结构,例如一个文件系统,是一个很好的类比。

[
 { type: 'dir', name: 'music', children: [
    { type: 'dir', name: 'genres', children: [
       { type: 'dir', name: 'triphop', children: [
          { type: 'file', name: 'Portishead' },
          ...
       ]},
     ]}
   ]}
]}

我有一个名为Thing 的递归组件,它采用childtree prop,看起来有点像这样:

<template>
  <input v-model="tree.name" />
  <thing v-for="childtree in tree.children" :tree="childtree" ></thing>
</template>

修改名字显然是要直接修改prop,这是要避免的,Vue会发出警告。

但是,我认为避免这种情况的唯一方法是让每个组件都对childtree 进行深层复制;然后$emit 复制我们的副本,并有一个@input(或类似的)将其复制回原件;一直到树上,然后会一直更改树下的道具,从而导致所有内容的另一个深层副本!

这感觉在任何大小的树上都会非常低效。

有没有更好的方法?我知道你可以欺骗 Vue 不发出错误/警告example jsfiddle

【问题讨论】:

  • 在你的例子中,'trick' 是可以的,而'normal' 不是,正是因为这个警告背后的原因;组件更新时会覆盖一个道具。双向数据流('trick' 中的 v-model)可能会大规模地对您不利。单向数据流($emit 或 Vuex)通常更可取。

标签: javascript vue.js vuejs2 vue-component


【解决方案1】:

解决您的问题的方法是仅从子组件传递事件。 https://jsfiddle.net/kv1w72pg/2/

<input @input="(e) => $emit('input', e.target.value)" />

话虽如此,我强烈建议您使用其他解决方案,例如:

  1. Vuex,
  2. 事件总线

它将使代码清晰并确保一个真实的来源。

编辑:在递归继承的情况下,使用提供注入会更容易:https://jsfiddle.net/s0b9fpr8/4/

这样你就提供了一个函数,可以将基础组件的状态更改为所有子组件(必须注入函数)。

【讨论】:

  • 谢谢。 $emit:问题是孩子的“价值”是孩子自己的价值观和它下面的整个子树。由于 javascript 通过引用传递,这将需要在每个阶段进行深度克隆/复制(昂贵、缓慢)。 Vuex、事件总线和提供/注入都是组件之间共享对象的方式,但在递归情况下,每个组件实例都需要处理嵌套属性,而不是顶级对象。因此,为了使用这些解决方案,它需要维护一个键列表以找到自己 - 如果其他节点发生更改,这将很难维护。
  • 它可以使用一个对象,您需要将它们在该对象上的位置传递给嵌套实例,以便它们知道要更改对象的哪一部分。
  • 是的,但前提是它们的祖先索引不会改变。例如在 root.a3.b2.c5 中找到的对象 - 如果在 a3 之前插入元素,则 a3 变为 a4。或链中的任何其他地方。
  • 这不是必须的,你可以使用一些独特的东西,比如路径:jsfiddle.net/7sojbcxk/4
【解决方案2】:

单向数据流

每次父组件更新时,子组件中的所有 props 都会刷新为最新的值。这意味着您不应该尝试改变子组件内的道具。如果你这样做了,Vue 会在控制台中警告你。

请注意,JavaScript 中的对象和数组是通过引用传递的,所以 如果 prop 是数组或对象,则改变对象或数组本身 子组件内部会影响父状态。

这是lodash的示例

当我们只是clone.

var objects = [{ 'a': 1 }, { 'b': 2 }];

var shallow = _.clone(objects);
console.log(shallow[0] === objects[0]);
// => true

当我们使用cloneDeep

var objects = [{ 'a': 1 }, { 'b': 2 }];

var deep = _.cloneDeep(objects);
console.log(deep[0] === objects[0]);
// => false

所以,如果要更新子组件中的任何 props 值,则需要使用cloneDeep of lodash。

【讨论】:

  • 谢谢,但 cloneDeep 是我想要避免的。这意味着每个节点都必须深度克隆它下面的整个树。
  • 并非如此,在您的最后一个组件中 [输入 el] 将实现深层克隆,您想要更改某些内容而不给父组件提供任何线索。
  • 我需要在每个级别更改一些内容(例如,在本示例中为目录名称)
【解决方案3】:

我大致是这样处理的:

<template>
  <div>
    <input v-model="myName" @input="updateParent()" />
    <thing v-for="(child, i) in myChildren" :key="child.uniqueId"
     @input="setChild(i, $event)"
     >
    </thing>
  </div>
</template>
<script>
export default {
props: ['node'],
data() {
  return {
     myName: this.node.name,
     myChildren: this.node.children.slice(0),
  };
},
methods: {
  updateParent() {
    this.$emit('input', {name: this.myName, children: this.myChildren});
  },
  setchild(i, newChild) {
    this.$set(this.myChildren, i, newChild);
  }
}
}
</script>

这意味着树的每个节点:

  • 使用它自己的“名称”副本
  • 拥有自己的子数组(即使子数组是“真实的”——它们是属于对象的引用,不是“我们的”要更改,但数组是我们的)。
  • 告诉父母在有更新时替换它。
  • 侦听已更新的子节点。

这似乎有效,并且似乎避免了变异道具。此处未显示但我发现至关重要的一件事是为每个孩子设置一个唯一的 ID。我通过从全局计数器($root)按顺序创建 Id 来做到这一点。

【讨论】:

    猜你喜欢
    • 2019-11-07
    • 2017-11-11
    • 2021-06-18
    • 2020-06-23
    • 1970-01-01
    • 2018-11-26
    • 2018-03-05
    • 2019-02-07
    • 2018-08-25
    相关资源
    最近更新 更多