Vue 组件中的道具是可变的吗?

Are the props in a Vue component mutable?

提问人:James Lin 提问时间:9/17/2021 最后编辑:benson23James Lin 更新时间:7/16/2022 访问量:3584

问:

人!最近,我正在做一个Vue.js项目,我想尽可能地将其拆分为小组件。有一个组件从父组件渲染列表,我想从道具中更改某些动作的一些属性。在这一点上,我只是好奇 props 变量在 Vue.js 组件中是否可变。

下面是一个示例代码,以便更好地理解:

<template>
    <div>
        <div
            class="d-flex align-items-center mb-2 justify-between"
            v-for="(option, index) in options"
            :key="index"
        >
            <b-input-group>
                <b-input-group-prepend>
                    <b-button variant="secondary">
                        <b-icon icon="grip-horizontal"></b-icon>
                    </b-button>
                </b-input-group-prepend>

                <b-form-input
                    placeholder="Option"
                    @input="updateOption(index, $event)"
                    :value="option.answer"
                ></b-form-input>

                <b-input-group-append>
                    <b-button variant="secondary" @click="removeOption(index)">
                        Remove
                    </b-button>
                </b-input-group-append>
            </b-input-group>
            <b-form-checkbox
                :id="`option-${index}`"
                name="options"
                class="f-14 text-muted ml-1"
                v-model="option.correct"
            >
                Correct?
            </b-form-checkbox>
        </div>
    </div>
</template>

<script>
export default {
    name: 'multi-options',
    props: ['options'],
    methods: {
        updateOption(index, value) {
            this.options[index].answer = value // Should this work or not?
        },
        removeOption(index) {
            this.options.splice(index, 1)
        },
        addOption() {
            this.options.push({
                answer: '',
                correct: false,
            })
        },
    },
}
</script>

我感谢任何评论。

JavaScript的 vue.js 可变 vue-props

评论


答:

0赞 Mythos 9/17/2021 #1

你不应该从组件本身改变道具。从文档来看,这是为了防止子组件意外更改父组件状态,这可能会使应用的数据流更难理解。

您可以做的是从子组件发出自定义事件:

updateOption(index, value) {
    this.$emit("myUpdateEvent", { index, value });
}
removeOption(index) {
    this.$emit("myRemoveEvent", { index });
}
addOption() {
    this.$emit("myAddEvent");
}

然后在父组件中侦听它们,并对作为道具传递给子组件的原始数组进行更改。

<multi-options @myAddEvent="handleAddEvent()" @myUpdateEvent="handleUpdateEvent($event.index, $event.value)" @myRemoveEvent="handleRemoveEvent($event.index)" :options="myOptions"/>
data() {
    return {
        myOptions: [],
    }
},
methods: {
    handleUpdateEvent(index, value) {
        this.myOptions[index].answer = value
    },
    handleRemoveEvent(index) {
        this.myOptions.splice(index, 1);
    },
    handleAddEvent() {
        this.myOptions.push({
                answer: '',
                correct: false,
            });
    }
}

评论

1赞 Luciano 9/17/2021
这将起作用,但您是通过向父级添加逻辑来耦合组件。在可重用性方面,子级应该操作数组并将突变的数据发送回父级。
0赞 Mythos 9/18/2021
是的,当我写答案时没有想到这一点。谢谢:)
0赞 Mythos 9/18/2021
我添加了一个新答案,显示了在解决我们两个答案中的问题时更好的方法
2赞 Luciano 9/17/2021 #2

不,绝不。父子之间的 Vue js 通信是单向的。从父母那里收到的道具不应该被变异,否则,如果你在启用严格模式的情况下工作,你会收到警告。

从父母到孩子的数据必须以 .从子项到父级的数据必须在propsevents

这里最好的方法是在子组件中本地操作数据,方法是在挂载组件时设置本地数据,然后在发生更改时向父组件发出事件(仅当需要让父组件知道发生了某些事情时)

props: ['options'],

data: () => ({
    local_options: []
}),

mounted () {
    this.local_options = [ ...this.options]
},

watch: {
   options: function () {
       deep: true,
       handler () {
          this.local_options = [ ...this.options]
       }
   }
},

methods: {
   updateOption(index, value) {
       this.local_options[index].answer = value
       this.$emit('updated', this.local_options)
   },

   removeOption(index) {
       this.local_options.splice(index, 1)
       this.$emit('updated', this.local_options)
   },

   addOption() {
       this.local_options.push({
           answer: '',
           correct: false,
       })
       this.$emit('updated', this.local_options)
   },
},

然后,在父级中,在父级中,您可以根据子级事件执行某些操作:

<multi-options-component :options="options" @updated="doSomething" />

....
methods: {
    doSomething (options) {
         // Some logic here
    }
}

评论

0赞 Mythos 9/18/2021
我建议你从每个方法中的道具中派生出来,而不是把它派生在钩子上,原因有两个:1.你必须手动确保两个选项始终同步,这意味着你基本上失去了 Vue 提供的反应优势。例如 可能由于某种原因而失败,现在您不同步。2. 如果您的子组件因任何原因从父组件更新(例如 已更新),将不会再次调用,因此您有问题。local_optionsmounteddoSomethinglocal_optionsoptionsmounted
0赞 Luciano 9/18/2021
很好,我为选项道具添加了一个深度观察器,因此如果父母更新选项,孩子也会这样做。
0赞 Mythos 9/18/2021
我添加了另一个答案,说明为什么出于多种原因这是一个坏主意,以及更好的方法。请检查。
2赞 Mythos 9/18/2021 #3

我相信 @Luciano 更喜欢为孩子设置一个单独的本地状态并手动维护其反应性的方式是灾难的根源(更不用说考虑到 Vue 已经内置了反应性,它的方式太过分了)。我在评论中提到了这一点,我将在这里再次添加:

  1. 如果由于任何原因失败,则父数组的数组将不会更新,而数组已在组件中更新。现在,父州和本地州不同步,您有问题。doSomethingoptionslocal_options
  2. 无需深入使用您构建的每个可重用组件。如果有数百个这样的子组件,则性能会受到影响,并且不必要地消耗大量内存来为每个子组件创建和维护单独的本地状态。watcher
  3. 你正在失去 Vue 在尝试手动确保(尽管是以一种容易出错的方式)本地状态始终与父级同步时提供的反应优势。这不是你应该如何使用Vue.js。

因此,我添加了另一个答案,该答案在可重用性和 DRY 原则方面比我之前的答案更好,同时也简单且无混乱:

您需要将数组与单个自定义事件一起传递回父级,但不是在生命周期挂钩中创建此数组,而是在每个方法中随时随地创建它:updatemounted

props: ['options'],

methods: {
   updateOption(index, value) {
       let local_options = [ ...this.options]
       local_options[index].answer = value
       this.$emit('update', local_options)
   },

   removeOption(index) {
       let local_options = [ ...this.options]
       local_options.splice(index, 1)
       this.$emit('update', local_options)
   },

   addOption() {
       let local_options = [ ...this.options]
       local_options.push({
           answer: '',
           correct: false,
       })
       this.$emit('update', local_options)
   },
},

然后,当子组件发出带有指令的事件或其简写时,更新父组件的数据optionupdatev-on:@

<multi-options :options="options" @update="handleUpdate" />

....
methods: {
    handleUpdate(options) {
         this.options = options; // this reassignment re-renders the child component
    }
}

编辑:我错过了你在元素中使用的道具。这将创建与 prop 的双向绑定,因为它转换为 .这意味着您正在更改子组件中的道具。请改为执行以下操作:v-model<b-form-checkbox>v-model="option.correct":checked="option.correct" @change="option.correct = $event"

            <b-form-checkbox
                :id="`option-${index}`"
                name="options"
                class="f-14 text-muted ml-1"
                :checked="option.correct"
                @change="setOptionsCorrect(index, $event)"
            >
                Correct?
            </b-form-checkbox>

将方法添加到您的:methods

setOptionsCorrect(index, value) {
    let local_options = [ ...this.options]
    local_options[index].correct = value
    this.$emit('update', local_options)
}

编辑 2:如果您坚持使用样式首选项,则可以从选项派生计算属性。但是你需要一个单独的 setter 将事件发出给父级,否则你会遇到同样的“变异道具”问题v-model

computed : {
    computedOptions: {
        get: function() {
            return this.options
        },
        set: function(newOptions) {
            this.$emit("update", newOptions)
        }
    }
},
methods: {
   updateOption(index, value) {
       let local_options = [ ...this.options]
       local_options[index].answer = value
       this.options = local_options
   },

   removeOption(index) {
       let local_options = [ ...this.options]
       local_options.splice(index, 1)
       this.options = local_options
   },

   addOption() {
       let local_options = [ ...this.options]
       local_options.push({
           answer: '',
           correct: false,
       })
       this.options = local_options
   },
}

评论

0赞 Luciano 9/18/2021
这是没有意义的,只要你在子组件中更改了绑定属性,比如 v-model=option.correct,你就会在发出事件之前直接改变道具。
0赞 Mythos 9/19/2021
@Luciano 这就是为什么你永远不应该在道具上使用 v-model,它会创建双向绑定。检查对我的答案的编辑
0赞 Luciano 9/19/2021
所以你建议不要使用 v-model 指令,而是在单向数据绑定中处理所有内容?我以为你想利用反应性......如果您愿意,可以将本地数据设置为初始数据状态,但您在此处所做的只会以大型组件的凌乱意大利面条代码结束。请阅读文档 vuejs.org/v2/guide/components-props.html#One-Way-Data-Flow
0赞 Mythos 9/19/2021
当然,如果 OP 想简单地将选项作为初始值传递,然后不更新父组件中的 prop,正如文档所说,那么在数据函数中设置它是正确的方法(不是在挂载的钩子中,不是在深度观察器中)。但是 OP 希望组件的状态与父组件的 options 数组同步,在这种情况下,这不是你应该做的。我利用了 Vue 的反应性,通过将组件绑定到 prop 而不是 local_options 来更新 prop 更改。您能否详细说明这将如何最终成为意大利面条代码?我渴望学习:)
0赞 Mythos 9/19/2021
此外,docs 中的属性是个好主意,除了我们必须向它添加 getter 和 setter 才能使其在 OP 的情况下工作。我也会将这种方式添加到我的答案中。computed