【问题标题】:Vue 2 - Mutating props vue-warnVue 2 - 变异道具 vue-warn
【发布时间】:2021-06-04 20:37:41
【问题描述】:

我开始https://laracasts.com/series/learning-vue-step-by-step 系列。我在 Vue、Laravel 和 AJAX 课程上停了下来,但出现了以下错误:

vue.js:2574 [Vue 警告]:避免直接改变 prop,因为每当父组件重新渲染时,该值都会被覆盖。相反,使用基于道具值的数据或计算属性。正在变异的道具:“列表”(在组件中找到)

我在 ma​​in.js

中有这段代码
Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    created() {
        this.list = JSON.parse(this.list);
    }
});
new Vue({
    el: '.container'
})

当我覆盖列表道具时,我知道问题出在 created() 中,但我是 Vue 的新手,所以我完全不知道如何解决它。任何人都知道如何(请解释原因)修复它吗?

【问题讨论】:

  • 猜猜,这只是一条警告消息,而不是错误。

标签: javascript vue.js vuejs2


【解决方案1】:

Vue 只是警告您:您更改了组件中的 prop,但是当父组件重新渲染时,“list”将被覆盖,您将丢失所有更改。所以这样做很危险。

像这样使用计算属性:

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    computed: {
        listJson: function(){
            return JSON.parse(this.list);
        }
    }
});

【讨论】:

  • 2 路绑定道具怎么样?
  • 如果你想要 2 路数据绑定,你应该使用自定义事件,更多信息请阅读:vuejs.org/v2/guide/components.html#sync-Modifier 你仍然不能直接修改道具,你需要一个事件和函数来处理道具更改给你。
【解决方案2】:

这与 mutating a prop locally is considered an anti-pattern in Vue 2

如果你想在本地改变一个 prop,你现在应该做的是在你的 data 中声明一个使用 props 值作为其初始值的字段,然后进行变异副本:

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    data: function () {
        return {
            mutableList: JSON.parse(this.list);
        }
    }
});

您可以在Vue.js official guide上阅读更多相关信息


注1:请注意you should not use the same name for your prop and data,即:

data: function () { return { list: JSON.parse(this.list) } } // WRONG!!

注意2:由于I feel there is some confusion关于propsreactivity,我建议你看看this线程

【讨论】:

  • 这是一个很好的答案,但我也认为应该更新它以包含事件绑定。从子级触发的事件可以更新父级,父级会将更新后的道具传递给子级。这样,仍然在所有组件中维护事实来源。还值得一提的是 Vuex 用于管理跨多个组件共享的状态。
  • @WesHarper,也可以使用.sync modifier作为简写,自动获取父节点的更新事件。
【解决方案3】:

Vue.js 认为这是一种反模式。例如,声明和设置一些 props,如

this.propsVal = 'new Props Value'

所以要解决这个问题,你必须从 props 中获取一个值到数据或 Vue 实例的计算属性中,如下所示:

props: ['propsVal'],
data: function() {
   return {
       propVal: this.propsVal
   };
},
methods: {
...
}

这肯定行得通。

【讨论】:

    【解决方案4】:

    如果你想改变道具 - 使用对象。

    <component :model="global.price"></component>
    

    组件:

    props: ['model'],
    methods: {
      changeValue: function() {
        this.model.value = "new value";
      }
    }
    

    【讨论】:

    • 就像一个注释,你在改变对象时必须小心。 Javascript 对象通过引用传递,但有一个警告:当您将变量设置为等于一个值时,引用会被破坏。
    【解决方案5】:

    我也遇到过这个问题。在我使用$on$emit 后警告消失了。 类似于使用$on$emit 推荐将数据从子组件发送到父组件。

    【讨论】:

      【解决方案6】:

      如果你使用 Lodash,你可以在返回之前克隆 prop。如果您在父子节点上修改该道具,此模式会很有帮助。

      假设我们在组件 grid 上有 prop list

      在父组件中

      <grid :list.sync="list"></grid>
      

      在子组件中

      props: ['list'],
      methods:{
          doSomethingOnClick(entry){
              let modifiedList = _.clone(this.list)
              modifiedList = _.uniq(modifiedList) // Removes duplicates
              this.$emit('update:list', modifiedList)
          }
      }
      

      【讨论】:

        【解决方案7】:

        道具下降,事件上升。这就是 Vue 的模式。关键是,如果您尝试改变从父级传递的道具。它不起作用,它只会被父组件反复覆盖。子组件只能发出事件通知父组件做某事。如果你不喜欢这些限制,你可以使用VUEX(实际上这种模式会吸收复杂的组件结构,你应该使用VUEX!)

        【讨论】:

        【解决方案8】:

        您不应更改子组件中道具的值。 如果你真的需要改变它,你可以使用.sync。 就这样

        &lt;your-component :list.sync="list"&gt;&lt;/your-component&gt;

        Vue.component('task', {
            template: '#task-template',
            props: ['list'],
            created() {
                this.$emit('update:list', JSON.parse(this.list))
            }
        });
        new Vue({
            el: '.container'
        })
        

        【讨论】:

          【解决方案9】:

          你需要像这样添加计算方法

          component.vue

          props: ['list'],
          computed: {
              listJson: function(){
                  return JSON.parse(this.list);
              }
          }
          

          【讨论】:

            【解决方案10】:

            Vue.js 的 props 不能被改变,因为这被认为是 Vue 中的反模式。

            您需要采取的方法是在您的组件上创建一个数据属性,该属性引用 list

            的原始 prop 属性
            props: ['list'],
            data: () {
              return {
                parsedList: JSON.parse(this.list)
              }
            }
            

            现在传递给组件的列表结构通过组件的data 属性被引用和变异:-)

            如果您希望做的不仅仅是解析列表属性,请使用 Vue 组件的 computed 属性。 这使您可以对道具进行更深入的突变。

            props: ['list'],
            computed: {
              filteredJSONList: () => {
                let parsedList = JSON.parse(this.list)
                let filteredList = parsedList.filter(listItem => listItem.active)
                console.log(filteredList)
                return filteredList
              }
            }
            

            上面的示例解析您的 list 属性并将其过滤为仅 活动 列表项,将其注销用于 schnitts 和 gggles 和返回它。

            注意datacomputed 属性在模板中引用相同,例如

            <pre>{{parsedList}}</pre>
            
            <pre>{{filteredJSONList}}</pre>
            

            很容易认为computed 属性(作为方法)需要被调用......它没有

            【讨论】:

              【解决方案11】:

              单向数据流, 根据https://vuejs.org/v2/guide/components.html,组件遵循单向 数据流, 所有的 props 在子属性和父属性之间形成一个单向绑定,当父属性更新时,它会向下流向子属性,而不是相反,这样可以防止子组件意外改变父组件,从而会使您的应用的数据流更难理解。

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

              通常有两种情况很容易对 prop 进行变异: prop 用于传入一个初始值;子组件之后希望将其用作本地数据属性。 prop 作为需要转换的原始值传入。 这些用例的正确答案是: 定义一个使用 prop 的初始值作为其初始值的本地数据属性:

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

              定义一个根据 prop 的值计算的计算属性:

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

              【讨论】:

                【解决方案12】:

                根据 VueJs 2.0,你不应该在组件内部改变一个 prop。他们只是由他们的父母变异。因此,您应该在 data 中定义不同名称的变量,并通过查看实际 props 来保持更新。 如果列表道具被父级更改,您可以对其进行解析并将其分配给 mutableList。这是一个完整的解决方案。

                Vue.component('task', {
                    template: ´<ul>
                                  <li v-for="item in mutableList">
                                      {{item.name}}
                                  </li>
                              </ul>´,
                    props: ['list'],
                    data: function () {
                        return {
                            mutableList = JSON.parse(this.list);
                        }
                    },
                    watch:{
                        list: function(){
                            this.mutableList = JSON.parse(this.list);
                        }
                    }
                });
                

                它使用 mutableList 来呈现您的模板,因此您可以在组件中保持列表道具的安全。

                【讨论】:

                • 手表用起来不贵吗?
                【解决方案13】:

                不要直接在组件中更改道具。如果需要更改,请设置一个新属性,如下所示:

                data () {
                    return () {
                        listClone: this.list
                    }
                }
                

                并改变listClone的值。

                【讨论】:

                  【解决方案14】:

                  Vue 模式是props 向下和events 向上。这听起来很简单,但在编写自定义组件时很容易忘记。

                  从 Vue 2.2.0 开始,您可以使用 v-model(和 computed properties)。我发现这种组合在组件之间创建了一个简单、干净且一致的接口:

                  • 传递给您的组件的任何props 都保持反应状态(即,它不会被克隆,也不需要watch 函数在检测到更改时更新本地副本)。
                  • 更改会自动发送给父级。
                  • 可与多个级别的组件一起使用。

                  计算属性允许单独定义 setter 和 getter。这允许Task 组件被重写如下:

                  Vue.component('Task', {
                      template: '#task-template',
                      props: ['list'],
                      model: {
                          prop: 'list',
                          event: 'listchange'
                      },
                      computed: {
                          listLocal: {
                              get: function() {
                                  return this.list
                              },
                              set: function(value) {
                                  this.$emit('listchange', value)
                              }
                          }
                      }
                  })  
                  

                  model 属性定义了哪个propv-model 相关联,以及哪个事件将在更改时发出。然后,您可以从父级调用此组件,如下所示:

                  <Task v-model="parentList"></Task>
                  

                  listLocal 计算属性在组件内提供了一个简单的 getter 和 setter 接口(将其视为私有变量)。在#task-template 中,您可以渲染listLocal,并且它将保持反应状态(即,如果parentList 更改,它将更新Task 组件)。您还可以通过调用 setter(例如,this.listLocal = newList)来改变 listLocal,它会将更改发送给父级。

                  这种模式的优点在于您可以将listLocal 传递给Task 的子组件(使用v-model),子组件的更改将传播到顶级组件。

                  例如,假设我们有一个单独的EditTask 组件,用于对任务数据进行某种类型的修改。通过使用相同的v-model 和计算属性模式,我们可以将listLocal 传递给组件(使用v-model):

                  <script type="text/x-template" id="task-template">
                      <div>
                          <EditTask v-model="listLocal"></EditTask>
                      </div>
                  </script>
                  

                  如果EditTask 发出更改,它将适当地在listLocal 上调用set(),从而将事件传播到顶层。同样,EditTask 组件也可以使用v-model 调用其他子组件(如表单元素)。

                  【讨论】:

                  • 我正在做类似的事情,除了同步。问题是我需要在发出更改事件后运行另一个方法,但是当方法运行并命中 getter 时,我会得到旧值,因为更改事件尚未被侦听器拾取/传播回子级。在这种情况下,我想改变道具,以便我的本地数据是正确的,直到父更新传播回来。在这方面有什么想法吗?
                  • 我怀疑从 setter 调用 getter 不是一个好习惯。您可能会考虑对您的计算属性进行监视并在那里添加您的逻辑。
                  • 道具下降和事件上升是我点击的。这在 VueJs 的文档中是在哪里解释的?
                  • 我认为这是将更改发送到父组件的更轻松的方式。
                  【解决方案15】:

                  除上述之外,对于其他有以下问题的人:

                  “如果不需要 props 值因此并不总是返回,则传递的数据将返回 undefined(而不是空)”。这可能会弄乱&lt;select&gt; 默认值,我通过检查该值是否在beforeMount() 中设置(如果没有设置)来解决它,如下所示:

                  JS:

                  export default {
                          name: 'user_register',
                          data: () => ({
                              oldDobMonthMutated: this.oldDobMonth,
                          }),
                          props: [
                              'oldDobMonth',
                              'dobMonths', //Used for the select loop
                          ],
                          beforeMount() {
                             if (!this.oldDobMonth) {
                                this.oldDobMonthMutated = '';
                             } else {
                                this.oldDobMonthMutated = this.oldDobMonth
                             }
                          }
                  }
                  

                  HTML:

                  <select v-model="oldDobMonthMutated" id="dob_months" name="dob_month">
                  
                   <option selected="selected" disabled="disabled" hidden="hidden" value="">
                   Select Month
                   </option>
                  
                   <option v-for="dobMonth in dobMonths"
                    :key="dobMonth.dob_month_slug"
                    :value="dobMonth.dob_month_slug">
                    {{ dobMonth.dob_month_name }}
                   </option>
                  
                  </select>
                  

                  【讨论】:

                    【解决方案16】:
                    Vue.component('task', {
                        template: '#task-template',
                        props: ['list'],
                        computed: {
                          middleData() {
                            return this.list
                          }
                        },
                        watch: {
                          list(newVal, oldVal) {
                            console.log(newVal)
                            this.newList = newVal
                          }
                        },
                        data() {
                          return {
                            newList: {}
                          }
                        }
                    });
                    new Vue({
                        el: '.container'
                    })
                    

                    也许这会满足您的需求。

                    【讨论】:

                      【解决方案17】:

                      添加到最佳答案,

                      Vue.component('task', {
                          template: '#task-template',
                          props: ['list'],
                          data: function () {
                              return {
                                  mutableList: JSON.parse(this.list);
                              }
                          }
                      });
                      

                      通过数组设置道具用于开发/原型设计,在生产中确保设置道具类型(https://vuejs.org/v2/guide/components-props.html)并设置默认值,以防父级未填充道具,因此。

                      Vue.component('task', {
                          template: '#task-template',
                          props: {
                            list: {
                              type: String,
                              default() {
                                return '{}'
                              }
                            }
                          },
                          data: function () {
                              return {
                                  mutableList: JSON.parse(this.list);
                              }
                          }
                      });
                      

                      这样你至少会在mutableList 中得到一个空对象,而不是 JSON.parse 错误(如果它未定义)。

                      【讨论】:

                        【解决方案18】:

                        我想给出这个答案,它有助于避免使用大量代码、观察者和计算属性。在某些情况下,这可能是一个很好的解决方案:

                        道具旨在提供单向交流。

                        当您有一个带有道具的模态show/hide 按钮时,对我来说最好的解决方案是发出一个事件:

                        <button @click="$emit('close')">Close Modal</button>
                        

                        然后将监听器添加到模态元素:

                        <modal :show="show" @close="show = false"></modal>
                        

                        (在这种情况下,show 可能是不必要的,因为您可以直接在基本模式上使用简单的v-if="show"

                        【讨论】:

                          【解决方案19】:

                          我个人总是建议如果你需要改变道具,首先将它们传递给计算属性并从那里返回,然后可以轻松地改变道具,即使你可以跟踪道具突变,如果那些正在也从另一个组件变异,或者我们也可以观看。

                          【讨论】:

                            【解决方案20】:

                            因为 Vue props 是一种数据流动方式,这可以防止子组件意外改变父组件的状态。

                            从Vue官方文档中,我们会找到解决这个问题的2种方法

                            1. 如果子组件想要使用 props 作为本地数据,最好定义一个本地数据属性。

                                props: ['list'],
                                data: function() {
                                  return {
                                    localList: JSON.parse(this.list);
                                  }
                                }
                              
                              
                            2. prop 作为需要转换的原始值传入。在这种情况下,最好使用 prop 的值来定义计算属性:

                                props: ['list'],
                                computed: {
                                  localList: function() {
                                     return JSON.parse(this.list);
                                  },
                                  //eg: if you want to filter this list
                                  validList: function() {
                                     return this.list.filter(product => product.isValid === true)
                                  }
                                  //...whatever to transform the list
                                }
                              
                              
                              

                            【讨论】:

                              【解决方案21】:

                              答案很简单,您应该通过将值分配给一些局部组件变量(可以是数据属性,使用 getter、setter 或 watchers 计算)来打破直接的 prop 突变。

                              这是一个使用观察者的简单解决方案。

                              <template>
                                <input
                                  v-model="input"
                                  @input="updateInput" 
                                  @change="updateInput"
                                />
                              
                              </template>
                              
                              <script>
                                export default {
                                props: {
                                  value: {
                                    type: String,
                                    default: '',
                                  },
                                },
                                data() {
                                  return {
                                    input: '',
                                  };
                                },
                                watch: {
                                  value: {
                                    handler(after) {
                                      this.input = after;
                                    },
                                    immediate: true,
                                  },
                                },
                                methods: {
                                  updateInput() {
                                    this.$emit('input', this.input);
                                  },
                                },
                              };
                              </script>
                              

                              我用它来创建任何数据输入组件,它工作得很好。任何从父级发送的新数据(v-model(ed))都将被值观察者监视并分配给输入变量,一旦接收到输入,我们就可以捕获该动作并将输入发送给父级,表明数据已输入来自表单元素。

                              【讨论】:

                              • 对 data.input 使用单词 'input' 恕我直言,当它与 @input 事件发生冲突时会让人感到困惑。从技术上讲,当从子视角内部看时,它将被输出。当父母订阅它时,它是来自孩子的“输入”。最好称它为 data.name 或 smth clear,不依赖于视角,也不会将名称与事件冲突
                              • @DimitryK 这是v-model 的问题。显然,人们已经失去了理智,并积极提倡在任何组件上使用 v-model,无论该组件是否与用户输入有任何关系。 Vuetify 这样做会对他们的 v-dialogv-bottom-sheet 组件产生巨大的负面影响。在对话框或底部工作表上收听@input 几乎是精神错乱,因为这两个组件都不接受任何类型的输入 - 并且像对待它们一样对它们进行编码是一种严重的反模式。
                              【解决方案22】:

                              是的!在 vue2 中改变属性是反模式。但... 用其他规则打破规则,继续前进! 您需要将 .sync 修饰符添加到 paret 范围内的组件属性。 &lt;your-awesome-components :custom-attribute-as-prob.sync="value" /&gt;

                              ? 我们杀死蝙蝠侠很简单?

                              【讨论】:

                                【解决方案23】:

                                当 TypeScript 是您的首选语言时。发展历程

                                <template>
                                <span class="someClassName">
                                      {{feesInLocale}}
                                </span>
                                </template>  
                                
                                
                                
                                @Prop({default: 0}) fees: any;
                                
                                // computed are declared with get before a function
                                get feesInLocale() {
                                    return this.fees;
                                }
                                

                                而不是

                                <template>
                                <span class="someClassName">
                                      {{feesInLocale}}
                                </span>
                                </template>  
                                
                                
                                
                                @Prop() fees: any = 0;
                                get feesInLocale() {
                                    return this.fees;
                                }
                                

                                【讨论】:

                                  【解决方案24】:

                                  下面是一个小吃店组件,当我像这样将 snackbar 变量直接放入 v-model 时,如果可以,但在控制台中,它会给出错误

                                  避免直接修改 prop,因为每当父组件重新渲染时,该值都会被覆盖。相反,使用基于道具值的数据或计算属性。

                                  <template>
                                          <v-snackbar v-model="snackbar">
                                          {{ text }}
                                        </v-snackbar>
                                  </template>
                                  
                                  <script>
                                      export default {
                                          name: "loader",
                                  
                                          props: {
                                              snackbar: {type: Boolean, required: true},
                                              text: {type: String, required: false, default: ""},
                                          },
                                  
                                      }
                                  </script>
                                  

                                  摆脱这种突变错误的正确方法是使用watcher

                                  <template>
                                          <v-snackbar v-model="snackbarData">
                                          {{ text }}
                                        </v-snackbar>
                                  </template>
                                  
                                  <script>
                                  /* eslint-disable */ 
                                      export default {
                                          name: "loader",
                                           data: () => ({
                                            snackbarData:false,
                                          }),
                                          props: {
                                              snackbar: {type: Boolean, required: true},
                                              text: {type: String, required: false, default: ""},
                                          },
                                          watch: { 
                                          snackbar: function(newVal, oldVal) { 
                                            this.snackbarData=!this.snackbarDatanewVal;
                                          }
                                        }
                                      }
                                  </script>
                                  

                                  因此,在您将加载此小吃店的主要组件中,您只需执行此代码

                                   <loader :snackbar="snackbarFlag" :text="snackText"></loader>
                                  
                                  

                                  这对我有用

                                  【讨论】:

                                    【解决方案25】:

                                    Vue3 有一个非常好的解决方案。花了几个小时才到达那里。但是效果真的很好。

                                    在父模板上

                                    <user-name
                                      v-model:first-name="firstName"
                                      v-model:last-name="lastName"
                                    ></user-name>
                                    

                                    子组件

                                    app.component('user-name', {
                                          props: {
                                          firstName: String,
                                          lastName: String
                                         },
                                    template: `
                                       <input 
                                      type="text"
                                      :value="firstName"
                                      @input="$emit('update:firstName', 
                                     $event.target.value)">
                                    
                                    <input
                                      type="text"
                                      :value="lastName"
                                       @input="$emit('update:lastName', 
                                        $event.target.value)">
                                      `
                                    })
                                    

                                    这是唯一进行双向绑定的解决方案。我喜欢前两个答案是使用 SYNC 和 Emitting 更新事件以及计算属性 getter setter 的好方法,但那是一项工作,我不喜欢这么努力。

                                    【讨论】:

                                      【解决方案26】:

                                      一个潜在的解决方案是使用全局变量。

                                      import { Vue } from "nuxt-property-decorator";
                                      
                                      export const globalStore = new Vue({
                                        data: {
                                          list: [],
                                        },
                                      }
                                      
                                      export function setupGlobalsStore() {
                                        Vue.prototype.$globals = globalStore;
                                      }
                                      

                                      然后你会使用:

                                      $globals.list
                                      

                                      您需要对其进行变异或呈现的任何地方。

                                      【讨论】:

                                      • 如果你这样做,那你为什么要使用Vue。全局变量不应用于局部变量。
                                      猜你喜欢
                                      • 2019-05-03
                                      • 2019-02-27
                                      • 2018-05-01
                                      • 2020-08-06
                                      • 2021-06-15
                                      • 2018-01-27
                                      • 1970-01-01
                                      • 2020-12-13
                                      相关资源
                                      最近更新 更多