Vue 2 - Мутирующие реквизиты vue-warn


181

Я начал серию https://laracasts.com/series/learning-vue-step-by-step . Я остановился на уроке Vue, Laravel и AJAX с этой ошибкой:

vue.js: 2574 [Vue warn]: избегайте прямого изменения свойства, так как значение будет перезаписываться при каждом повторном рендеринге родительского компонента. Вместо этого используйте данные или вычисляемое свойство, основанное на значении реквизита. Пропуская мутирование: «список» (находится в компоненте)

У меня есть этот код в main.js

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

Я знаю, что проблема в create (), когда я перезаписываю реквизит списка, но я новичок в Vue, поэтому я совершенно не знаю, как это исправить. У кого-нибудь есть идеи, как (и, пожалуйста, объясните, почему) это исправить?


2
Угадайте, это просто предупреждающее сообщение, а не ошибка.
Дэвид Р

Ответы:


264

Это связано с тем фактом, что мутирование опоры локально считается анти-паттерном в Vue 2

Что вы должны сделать сейчас, если вы хотите локально изменить мутацию , это объявить поле в вашем поле, dataкоторое использует propsзначение в качестве начального значения, а затем изменить копию:

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

Вы можете прочитать больше об этом в официальном руководстве Vue.js


Примечание 1. Обратите внимание, что вы не должны использовать одно и то же имя для вашего propиdata , то есть:

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

Примечание 2: Так как я чувствую , что есть некоторая путаница относительно propsи реакционная способность , я предлагаю вам взглянуть на эту нить


3
Это отличный ответ, но я также думаю, что его следует обновить, чтобы включить привязку событий. Событие, инициированное дочерним элементом, может обновить родительский элемент, который передаст обновленный реквизит дочернему элементу. Таким образом, источник истины все еще поддерживается во всех компонентах. Также стоит упомянуть Vuex для управления состоянием, которое используется несколькими компонентами.
Уэс Харпер

@WesHarper, также можно использовать модификатор .sync в качестве сокращения для автоматического получения события обновления родителя.
danb4r

48

Шаблон Vue propsвниз и eventsвверх. Звучит просто, но легко забыть при написании пользовательского компонента.

Начиная с Vue 2.2.0 вы можете использовать v-модельвычисленными свойствами ). Я обнаружил, что эта комбинация создает простой, чистый и согласованный интерфейс между компонентами:

  • Любой propsпередаваемый компоненту остается реактивным (т. Е. Он не клонируется и не требует watchфункции для обновления локальной копии при обнаружении изменений).
  • Изменения автоматически отправляются родителю.
  • Может использоваться с несколькими уровнями компонентов.

Вычисленное свойство позволяет устанавливать и устанавливать и устанавливать отдельно. Это позволяет 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>

Свойство listLocalcomputed обеспечивает простой интерфейс метода получения и установки в компоненте (думайте, что это частная переменная). Внутри #task-templateвы можете выполнить рендеринг, listLocalи он останется реактивным (т. Е. При parentListизменении он обновит Taskкомпонент). Вы также можете изменить listLocalего, вызвав сеттер (например, this.listLocal = newList), и он отправит изменения родителю.

Что хорошо в этом шаблоне, так это то, что вы можете перейти listLocalк дочернему компоненту Task(using 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испускает изменение будет соответствующим образом позвонить set()на listLocalи тем самым распространить событие до уровня верхнего. Точно так же EditTaskкомпонент может также вызывать другие дочерние компоненты (такие как элементы формы), используя v-model.


1
Я делаю нечто подобное, кроме как с синхронизацией. Проблема в том, что мне нужно запустить другой метод после выдачи события change, однако, когда метод запускается и попадает в метод получения, я получаю старое значение, потому что событие change еще не было получено слушателем / передано обратно дочернему элементу. Это тот случай, когда я хочу изменить опору, чтобы мои локальные данные были правильными, пока родительское обновление не распространится обратно вниз. Есть мысли на этот счет?
koga73

Я подозреваю, что вызывать геттер от сеттера не очень хорошая практика. То, что вы могли бы рассмотреть, это поместить часы в вычисляемое свойство и добавить туда свою логику.
Крис

опора и события вверх - вот что щелкнуло мной. Где это объясняется в документах VueJ?
nebulousGirl

@nebulousGirl Посмотрите здесь: vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
Крис

Я думаю, что это более безболезненный способ внесения изменений в родительский компонент.
Мухаммед

36

Vue просто предупреждает вас: вы меняете реквизит в компоненте, но при повторном рендеринге родительского компонента «список» будет перезаписан, и вы потеряете все свои изменения. Так что это опасно.

Используйте вычисленное свойство вместо этого:

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

25
Как насчет двухсторонней привязки?
Джош Р.

7
Если вы хотите двухстороннее связывание данных, вы должны использовать пользовательские события, для получения дополнительной информации читайте: vuejs.org/v2/guide/components.html#sync-Modifier Вы все еще не можете напрямую изменить реквизит, вам нужны событие и функция, которые обрабатывает замену опоры для вас.
Марко

22

Если вы используете Lodash, вы можете клонировать опору, прежде чем вернуть ее. Этот шаблон полезен, если вы изменяете эту опору как на родителя, так и на ребенка.

Допустим, у нас есть список опор в сетке компонентов .

В родительском компоненте

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

19

Опоры вниз, события вверх. Это шаблон Vue. Дело в том, что если вы попытаетесь изменить пропуски, передаваемые от родителя. Он не будет работать, и он просто перезаписывается родительским компонентом. Дочерний компонент может генерировать только событие, чтобы уведомить родительский компонент о выполнении чего-либо. Если вам не нравятся эти ограничения, вы можете использовать VUEX (на самом деле этот шаблон будет засасывать сложную структуру компонентов, вам следует использовать VUEX!)


2
Также есть возможность использовать шину событий
Стивен Прибилинский

Шина событий изначально не поддерживается в vue 3. github.com/vuejs/rfcs/pull/118
AlexMA

15

Вы не должны изменять значение реквизита в дочернем компоненте. Если вам действительно нужно изменить его, вы можете использовать .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'
})

7

Согласно VueJs 2.0, вы не должны мутировать пропеллер внутри компонента. Они только видоизменены своими родителями. Следовательно, вы должны определять переменные в данных с разными именами и обновлять их, наблюдая за фактическими данными. В случае, если реквизиты списка изменены родителем, вы можете проанализировать его и назначить его в 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 для визуализации вашего шаблона, таким образом, вы сохраняете свой список в безопасности в компоненте.


не стоит ли смотреть дорогие в использовании?
Kick Buttowski

6

не меняйте реквизиты непосредственно в компонентах. если вам нужно изменить их, установите новое свойство, например:

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

И измените значение listClone.


6

Ответ прост: вы должны прервать прямую мутацию, присвоив значение некоторым локальным переменным компонента (это может быть свойство данных, вычисленное с помощью геттеров, сеттеров или наблюдателей).

Вот простое решение с помощью наблюдателя.

<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-модель (ed)) от родителя, будут отслеживаться наблюдателем значения и назначаться входной переменной, и после получения ввода мы можем перехватить это действие и передать входные данные родителю, предполагая, что данные вводятся. из элемента формы.


5

Я тоже столкнулся с этой проблемой. Предупреждение ушло после того, как я использую $onи $emit. Это что-то вроде использования $onи $emitрекомендуется отправлять данные из дочернего компонента в родительский компонент.


4

Если вы хотите поменять реквизит - используйте объект.

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

составная часть:

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

Так же, как примечание, вы должны быть осторожны при мутировании объектов. Объекты Javascript передаются по ссылке, но есть предостережение: ссылка прерывается, когда вы устанавливаете переменную, равную значению.
ира

3

Вы должны добавить вычисляемый метод, как это

component.vue

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

3

Односторонний поток данных, согласно https://vuejs.org/v2/guide/components.html , компонент следует одностороннему потоку данных. Все реквизиты образуют одностороннюю привязку между дочерним свойством и родительским Во-первых, когда родительское свойство обновляется, оно передается дочернему элементу, но не наоборот, это предотвращает случайное изменение дочерних компонентов родительского, что может затруднить понимание потока данных вашего приложения.

Кроме того, каждый раз, когда родительский компонент обновляется, все реквизиты в дочерних компонентах обновляются с последним значением. Это означает, что вы не должны пытаться изменить опору внутри дочернего компонента. Если вы это сделаете .vue предупредит вас в консоли.

Обычно есть два случая, когда возникает соблазн мутировать реквизит: реквизит используется для передачи начального значения; потом дочерний компонент хочет использовать его как локальное свойство данных. Опора передается как необработанная ценность, которую необходимо преобразовать. Правильный ответ на эти случаи использования: Определите локальное свойство данных, которое использует начальное значение проп в качестве своего начального значения:

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

Определите вычисляемое свойство, которое вычисляется из значения проп:

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

2

Реквизиты Vue.js не должны быть видоизменены, так как это считается Anti-Pattern в Vue.

Подход, который вам потребуется, - это создать свойство данных в вашем компоненте, которое ссылается на исходное свойство 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 и хихикает и возвращает его.

примечание : оба шаблона dataи computedсвойства указаны в шаблоне одинаково, например

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

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

Может быть легко предположить, что computedсвойство (будучи методом) нужно вызывать ... это не так


2
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'
})

Может быть, это будет соответствовать вашим потребностям.


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, если он не определен.


1

Ниже приведен компонент снек-бара, когда я передаю переменную снэк-бара непосредственно в v-модель, как это, если она будет работать, но в консоли, она выдаст ошибку как

Избегайте прямого изменения свойства, так как значение будет перезаписываться при повторном рендеринге родительского компонента. Вместо этого используйте данные или вычисляемое свойство, основанное на значении реквизита.

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

Это сработало для меня


0

Vue.js считает это анти-паттерном. Например, объявив и установив некоторые реквизиты, такие как

this.propsVal = 'new Props Value'

Таким образом, чтобы решить эту проблему, вы должны получить значение из реквизита для данных или вычисляемое свойство экземпляра Vue, например:

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

Это определенно будет работать.


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>

0

Я хочу дать этот ответ, который поможет избежать использования большого количества кода, наблюдателей и вычисляемых свойств. В некоторых случаях это может быть хорошим решением:

Опоры предназначены для обеспечения односторонней связи.

Когда у вас есть модальная show/hideкнопка с опорой, лучшее решение для меня - это создать событие:

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

Затем добавьте слушателя к модальному элементу:

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

(В этом случае опора show, вероятно, не нужна, потому что вы можете использовать легкий v-if="show"прямо на базовый модальный)


0

Лично я всегда предлагаю, если вам нужно изменить мутацию, сначала передать их в вычисляемое свойство и вернуться оттуда, после чего можно легко изменить мутацию, даже при этом вы можете отслеживать мутацию винта, если он мутировал из другого. компонент тоже, или мы можем также посмотреть.


0

Поскольку Vue props является односторонним потоком данных, это предотвращает случайное изменение дочерних компонентов состояния родителя.

Из официального документа Vue мы найдем 2 способа решения этой проблемы.

  1. если дочерний компонент хочет использовать реквизиты в качестве локальных данных, лучше всего определить свойство локальных данных.

      props: ['list'],
      data: function() {
        return {
          localList: JSON.parse(this.list);
        }
      }
    
  2. Опора передается как необработанная ценность, которую необходимо преобразовать. В этом случае лучше всего определить вычисляемое свойство, используя значение проп:

      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
      }
    
    

0

ДА !, мутация атрибутов в vue2 - это анти-паттерн. НО ... Просто нарушай правила, используя другие правила, и иди вперед! Вам нужно добавить модификатор .sync к атрибуту вашего компонента в области видимости. <your-awesome-components :custom-attribute-as-prob.sync="value" />

🤡 Это просто, мы убиваем Бэтмена 😁


0

Когда 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;
}
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.