Javascript 如何";避免直接对道具进行变异;在递归组件中

Javascript 如何";避免直接对道具进行变异;在递归组件中,javascript,vue.js,vuejs2,vue-component,Javascript,Vue.js,Vuejs2,Vue Component,我理解这个概念:子对象处理其自己的道具数据副本,当它被更改时,它可以$emit进行更改,以便父对象可以更新自己 然而,我处理的是递归树结构,例如文件系统,这是一个很好的类比 [ { type: 'dir', name: 'music', children: [ { type: 'dir', name: 'genres', children: [ { type: 'dir', name: 'triphop', children: [ { type: 'f

我理解这个概念:子对象处理其自己的道具数据副本,当它被更改时,它可以
$emit
进行更改,以便父对象可以更新自己

然而,我处理的是递归树结构,例如文件系统,这是一个很好的类比

[
 { type: 'dir', name: 'music', children: [
    { type: 'dir', name: 'genres', children: [
       { type: 'dir', name: 'triphop', children: [
          { type: 'file', name: 'Portishead' },
          ...
       ]},
     ]}
   ]}
]}
我有一个名为
Thing
的递归组件,它采用
childtree
属性,看起来有点像这样:


修改名称显然会直接修改道具,这是要避免的,Vue会发出警告

然而,我能看到的避免这种情况的唯一方法是每个组件都做一个
childtree
的深度拷贝;然后
$emit
复制我们的副本,并让
@input
(或类似)将其复制回原始版本;一路爬到树上,这会改变树下的道具,造成另一个所有东西的深度复制

这让人感觉在任何大小的树上都是非常低效的


有更好的办法吗?我知道您可以欺骗Vue,使其不发出错误/警告。

解决您的问题的方法是仅从子组件传递事件。


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

  • Vuex
  • 事件总线
  • 它将使代码变得清晰,并确保一个真相的来源

    编辑:在递归继承的情况下,使用提供注入将更容易:


    通过这种方式,您可以提供一个函数,将基本组件的状态更改为所有子组件(必须注入函数)。

    单向数据流

    每次更新父组件时,子组件中的所有道具都将使用最新值刷新。这意味着您不应尝试在子组件内变异道具。如果您这样做,Vue将在控制台中警告您

    请注意,JavaScript中的对象和数组是通过引用传递的,因此 如果道具是数组或对象,则对对象或数组本身进行变异 子组件内部将影响父状态

    下面是一个使用

    当我们刚开始的时候

    当我们使用


    因此,如果您想更新子组件中的任何道具值,您需要使用lodash。

    以下是我大致的处理方法:

    <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>
    
    
    导出默认值{
    道具:['node'],
    数据(){
    返回{
    myName:this.node.name,
    myChildren:this.node.children.slice(0),
    };
    },
    方法:{
    updateParent(){
    this.$emit('input',{name:this.myName,children:this.myChildren});
    },
    setchild(i,newChild){
    this.$set(this.myChildren,i,newChild);
    }
    }
    }
    
    这意味着树的每个节点:

    • 在自己的“名称”副本上工作
    • 保存自己的子对象数组(即使子对象是“真实的”-它们是属于对象的引用,不属于要更改的“我们的”,但数组是我们的)
    • 通知父级在更新时替换它
    • 侦听已更新的子节点

    这似乎有效,似乎避免了道具的变异。这里没有显示一件事,但我发现很关键,那就是为每个孩子设置一个唯一的ID。我通过从全局计数器(在
    $root
    上)顺序创建ID来实现这一点。

    在您的示例中,“技巧”可以,而“正常”则不行,这正是因为此警告背后的原因;道具在组件更新时被覆盖。双向数据流(技巧中的v模型)可能在大规模上对您不利。单向数据流($emit或Vuex)通常更可取。谢谢,但cloneDeep是我想要避免的。这意味着每个节点都必须深度克隆它下面的整个树。实际上,在您的最后一个组件中[input el]将实现深度克隆,您希望在不向父组件提供任何线索的情况下更改某些内容。我需要在每个级别(如本例中的目录名)更改某些内容,谢谢<代码>$emit:问题是子级的“值”是子级自己的值及其下的整个子树。由于javascript是通过引用传递的,所以在每个阶段都需要进行深度克隆/复制(代价昂贵,速度慢)。Vuex、事件总线和提供/注入都是组件之间共享对象的方式,但在递归情况下,每个组件实例都需要处理嵌套属性,而不是顶级对象。因此,为了使用这些解决方案,它需要维护一个密钥列表来找到自己——这就是v。如果其他节点发生更改,则很难维护。它可以使用一个对象,您需要的是将它们在该对象上的位置传递给嵌套实例,以便它们知道要更改对象的哪一部分。是的,但前提是它们的祖先索引不会更改。e、 g.在根a3.b2.c5处找到的对象-如果在a3之前插入元素,则a3变为a4。或者链中的任何其他位置。这不是必需的,您可以使用一些独特的方法,例如路径:
    var objects = [{ 'a': 1 }, { 'b': 2 }];
    
    var shallow = _.clone(objects);
    console.log(shallow[0] === objects[0]);
    // => true
    
    var objects = [{ 'a': 1 }, { 'b': 2 }];
    
    var deep = _.cloneDeep(objects);
    console.log(deep[0] === objects[0]);
    // => false
    
    <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>