问题
I have a Modal
component in my main app that gets passed content via an event whenever a modal has to be shown. Modal content is always a list with an action associated with each item, like "select" or "remove":
Vue.component('modal', {
data() {
return {
shown: false,
items: [],
callback: ()=>{}
}
},
mounted() {
EventBus.$on('showModal', this.show);
},
template: `<ul v-if="shown">
<li v-for="item in items">
{{ item }} <button @click="callback(item)">Remove</button>
</li>
</ul>`,
methods: {
show(items, callback) {
this.shown = true;
this.items = items;
this.callback = callback;
}
}
});
Sadly, when passing a computed property to that modal like in the component below, the reactive link gets broken -> if the action is "remove", the list is not updated.
Vue.component('comp', {
data() {
return {obj: {a: 'foo', b: 'bar'}}
},
computed: {
objKeys() {
return Object.keys(this.obj);
}
},
template: `<div>
<button @click="showModal">Show Modal</button>
<modal></modal>
</div>`,
methods: {
remove(name) {
this.$delete(this.obj, name);
},
showModal() {
EventBus.$emit('showModal', this.objKeys, this.remove);
}
}
});
See the minimal use case in this fiddle: https://jsfiddle.net/christophfriedrich/cm778wgj/14/
I think this is a bug - shouldn't Vue remember that objKeys
is used for rendering in Modal
and update it? (The forwarding of the change of obj
to objKeys
works.) If not, what am I getting wrong and how could I achieve my desired result?
回答1:
You have the modal working with its own copy of items
:
template: `<ul v-if="shown">
<li v-for="item in items">
{{ item }} <button @click="callback(item)">Remove</button>
</li>
</ul>`,
methods: {
show(items, callback) {
this.shown = true;
this.items = items;
this.callback = callback;
}
}
That copy is made once, upon the call to show
, and what you are copying is just the value of the computed at the time you emit the showModal
event. What show
receives is not a computed, and what it assigns is not a computed. It's just a value.
If, anywhere in your code, you made an assignment like
someDataItem = someComputed;
the data item would not be a functional copy of the computed, it would be a snapshot of its value at the time of the assignment. This is why copying values around in Vue is a bad practice: they don't automatically stay in sync.
Instead of copying values around, you can pass a function that returns the value of interest; effectively a get function. For syntactic clarity, you can make a computed based on that function. Then your code becomes
const EventBus = new Vue();
Vue.component('comp', {
data() {
return {
obj: {
a: 'foo',
b: 'bar'
}
}
},
computed: {
objKeys() {
return Object.keys(this.obj);
}
},
template: `<div>
<div>Entire object: {{ obj }}</div>
<div>Just the keys: {{ objKeys }}</div>
<button @click="remove('a')">Remove a</button>
<button @click="remove('b')">Remove b</button>
<button @click="showModal">Show Modal</button>
<modal></modal>
</div>`,
methods: {
remove(name) {
this.$delete(this.obj, name);
},
showModal() {
EventBus.$emit('showModal', () => this.objKeys, this.remove);
}
}
});
Vue.component('modal', {
data() {
return {
shown: false,
getItems: null,
callback: () => {}
}
},
mounted() {
EventBus.$on('showModal', this.show);
},
template: `<div v-if="shown">
<ul v-if="items.length>0">
<li v-for="item in items">
{{ item }} <button @click="callback(item)">Remove</button>
</li>
</ul>
<em v-else>empty</em>
</div>`,
computed: {
items() {
return this.getItems && this.getItems();
}
},
methods: {
show(getItems, callback) {
this.shown = true;
this.getItems = getItems;
this.callback = callback;
}
}
});
var app = new Vue({
el: '#app'
})
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<div id="app">
<comp></comp>
</div>
回答2:
You are passing a value to a function, you are not passing a prop to a component. Props are reactive, but values are just values. You include modal
in the template of comp
, so rework it to take (at least) items
as a prop. Then it will be reactive.
I would recommend having the remove process follow the emit-event-and-process-in-parent rather than passing a callback.
const EventBus = new Vue();
Vue.component('comp', {
data() {
return {
obj: {
a: 'foo',
b: 'bar'
}
}
},
computed: {
objKeys() {
return Object.keys(this.obj);
}
},
template: `<div>
<div>Entire object: {{ obj }}</div>
<div>Just the keys: {{ objKeys }}</div>
<button @click="remove('a')">Remove a</button>
<button @click="remove('b')">Remove b</button>
<button @click="showModal">Show Modal</button>
<modal :items="objKeys" event-name="remove" @remove="remove"></modal>
</div>`,
methods: {
remove(name) {
this.$delete(this.obj, name);
},
showModal() {
EventBus.$emit('showModal');
}
}
});
Vue.component('modal', {
props: ['items', 'eventName'],
data() {
return {
shown: false,
}
},
mounted() {
EventBus.$on('showModal', this.show);
},
template: `<div v-if="shown">
<ul v-if="items.length>0">
<li v-for="item in items">
{{ item }} <button @click="emitEvent(item)">Remove</button>
</li>
</ul>
<em v-else>empty</em>
</div>`,
methods: {
show(items, callback) {
this.shown = true;
},
emitEvent(item) {
this.$emit(this.eventName, item);
}
}
});
var app = new Vue({
el: '#app'
})
<script src="//unpkg.com/vue@latest/dist/vue.js"></script>
<div id="app">
<comp></comp>
</div>
来源:https://stackoverflow.com/questions/49259249/vue-js-computed-property-loses-its-reactivity-when-passed-through-an-event