268

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

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

我在main.js中有这段代码

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

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

4

27 回答 27

335

这与在 Vue 2 中对 prop 进行本地变异被视为反模式这一事实有关

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

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

您可以在Vue.js 官方指南上阅读更多相关信息


注意 1:请注意,您的and不应使用相同的名称propdata,即:

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

注2:由于我觉得关于反应性和反应有些混乱,我建议你看看这个线程props

于 2016-10-07T05:47:30.000 回答
96

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

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

  • 传递给您的组件的任何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)
            }
        }
    }
})  

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

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

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

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

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

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

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

于 2018-08-07T08:21:43.133 回答
43

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

像这样使用计算属性:

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    computed: {
        listJson: function(){
            return JSON.parse(this.list);
        }
    }
});
于 2016-10-05T09:13:02.997 回答
26

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

假设我们在组件grid上有道具列表

在父组件中

<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)
    }
}
于 2017-07-21T18:34:42.307 回答
24

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

于 2017-08-15T13:05:10.680 回答
20

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

<your-component :list.sync="list"></your-component>

Vue.component('task', {
    template: '#task-template',
    props: ['list'],
    created() {
        this.$emit('update:list', JSON.parse(this.list))
    }
});
new Vue({
    el: '.container'
})
于 2017-10-24T10:42:29.660 回答
9

答案很简单,您应该通过将值分配给一些局部组件变量(可以是数据属性,使用 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))都将被值观察者监视并分配给输入变量,一旦接收到输入,我们就可以捕获该动作并将输入发送给父级,表明数据已输入从表单元素。

于 2019-11-26T09:42:55.177 回答
9

根据 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 来呈现您的模板,因此您可以在组件中保持列表道具的安全。

于 2018-02-06T08:15:58.297 回答
8

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

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

并更改 listClone 的值。

于 2018-03-01T17:27:38.143 回答
6

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

于 2017-06-15T13:55:26.707 回答
5

如果你想改变道具 - 使用 object.

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

零件:

props: ['model'],
methods: {
  changeValue: function() {
    this.model.value = "new value";
  }
}
于 2017-05-08T07:05:18.460 回答
4

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

道具旨在提供单向通信。

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

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

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

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

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

于 2019-09-27T05:51:52.880 回答
4

单向数据流,根据 https://vuejs.org/v2/guide/components.html,组件遵循单向数据流,所有的 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()
  }
}
于 2018-01-04T01:44:25.000 回答
4
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'
})

也许这将满足您的需求。

于 2018-12-14T09:56:31.233 回答
4

您需要添加这样的计算方法

组件.vue

props: ['list'],
computed: {
    listJson: function(){
        return JSON.parse(this.list);
    }
}
于 2017-12-21T10:39:11.387 回答
3

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
  }
}

上面的示例解析您的列表道具并将其过滤为仅活动列表项,将其注销以查看 schnitts 和 gggles并返回它。

注意:模板中引用的两个data&computed属性相同,例如

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

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

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

于 2017-12-29T10:54:53.887 回答
2

添加到最佳答案,

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 错误。

于 2019-02-02T07:55:29.560 回答
2

当 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;
}
于 2020-05-25T09:07:43.937 回答
2

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 的好方法,但那是一项工作,我不喜欢这么努力。

于 2020-09-22T21:41:13.833 回答
1

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

我们杀死蝙蝠侠很简单

于 2020-01-03T00:43:42.707 回答
1

Below is a snack bar component, when I give the snackbar variable directly into v-model like this if will work but in the console, it will give an error as

Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value.

<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>

Correct Way to get rid of this mutation error is use 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>

So in the main component where you will load this snack bar you can just do this code

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

This Worked for me

于 2020-06-19T08:10:20.797 回答
0

将道具分配给新变量。

data () {
    return {
        listClone: this.list
    }
}
于 2022-02-23T18:32:20.450 回答
0

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

于 2019-10-19T14:11:26.693 回答
0

因为 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
      }
    
    
    
于 2019-11-18T06:35:44.123 回答
0

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

this.propsVal = 'new Props Value'

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

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

这肯定会奏效。

于 2017-05-07T07:03:37.807 回答
0

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

“如果不需要道具值因此并不总是返回,则传递的数据将返回undefined(而不是空的)”。这可能会弄乱<select>默认值,我通过检查是否设置了值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>
于 2018-11-16T11:30:14.427 回答
-1

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

import { Vue } from "nuxt-property-decorator";

export const globalStore = new Vue({
  data: {
    list: [],
  },
}

export function setupGlobalsStore() {
  Vue.prototype.$globals = globalStore;
}

然后你会使用:

$globals.list

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

于 2021-09-23T00:35:41.383 回答