I've been using a global event bus for quite some time in Vue - something like const bus = new Vue()
. Works fine, however, disposal of subscriptions can bee quite verbose.
Let's say that I subscribe to an event in a ponent:
mounted() {
bus.$on('some.event', callback)
}
I would have to keep track of the callback and dispose it properly in beforeDestroy
. This can be somewhat simplified using a global mixin, but since I'm using <keep-alive>
, I have to differentiate between subscriptions made in mounted
and activated
callback.
So I figured I would give Vuex a shot at managing this, since watchers are disposed by the framework. I came up with the proposal below.
Seems to work fine as long as objects or arrays are published. Primitive data doesn't seem to trigger the reactivity, despite being wrapped in the outer object, i.e. { data: 123 }
I'm looking for alternative solutions regarding notifying the subscribers. All I've seen so far is the internal notify
method, which doesn't feel very safe to use.
eventstore.js
import Vue from 'vue'
const state = {
events: {}
}
const actions = {
publish({mit}, payload) {
mit('publish_event', payload)
}
}
const mutations = {
publish_event(state, payload) {
if(!state.events[payload.key]) {
Vue.set(state.events, payload.key, { data: payload.data })
} else {
state.events[payload.key] = { data: payload.data }
}
}
}
const getters = {
events: state => state.events
}
export default {
state,
actions,
mutations,
getters
}
globalmixin.js
methods: {
publish(key, data) {
this.$store.dispatch('publish', { key, data })
}
}
someponent.vue
function mapEventGetters(eventKeys) {
return _.reduce(eventKeys, (result, current) => {
result[current] = function() {
return _.get(this, `$store.getters.events.${current}.data`)
}
return result
}, {})
}
puted: {
...mapEventGetters(['foo_bar'])
},
watch: {
'foo_bar'(value) {
console.log(`foo_bar changed to ${value}`)
}
}
I've been using a global event bus for quite some time in Vue - something like const bus = new Vue()
. Works fine, however, disposal of subscriptions can bee quite verbose.
Let's say that I subscribe to an event in a ponent:
mounted() {
bus.$on('some.event', callback)
}
I would have to keep track of the callback and dispose it properly in beforeDestroy
. This can be somewhat simplified using a global mixin, but since I'm using <keep-alive>
, I have to differentiate between subscriptions made in mounted
and activated
callback.
So I figured I would give Vuex a shot at managing this, since watchers are disposed by the framework. I came up with the proposal below.
Seems to work fine as long as objects or arrays are published. Primitive data doesn't seem to trigger the reactivity, despite being wrapped in the outer object, i.e. { data: 123 }
I'm looking for alternative solutions regarding notifying the subscribers. All I've seen so far is the internal notify
method, which doesn't feel very safe to use.
eventstore.js
import Vue from 'vue'
const state = {
events: {}
}
const actions = {
publish({mit}, payload) {
mit('publish_event', payload)
}
}
const mutations = {
publish_event(state, payload) {
if(!state.events[payload.key]) {
Vue.set(state.events, payload.key, { data: payload.data })
} else {
state.events[payload.key] = { data: payload.data }
}
}
}
const getters = {
events: state => state.events
}
export default {
state,
actions,
mutations,
getters
}
globalmixin.js
methods: {
publish(key, data) {
this.$store.dispatch('publish', { key, data })
}
}
someponent.vue
function mapEventGetters(eventKeys) {
return _.reduce(eventKeys, (result, current) => {
result[current] = function() {
return _.get(this, `$store.getters.events.${current}.data`)
}
return result
}, {})
}
puted: {
...mapEventGetters(['foo_bar'])
},
watch: {
'foo_bar'(value) {
console.log(`foo_bar changed to ${value}`)
}
}
Share
Improve this question
edited Dec 6, 2018 at 9:10
Johan
asked Dec 3, 2018 at 16:32
JohanJohan
35.2k62 gold badges187 silver badges305 bronze badges
12
- What is 'the internal notify method'? Are referring to watchers? – Richard Matsen Commented Dec 3, 2018 at 18:36
- @RichardMatsen Here's an example: github./vuejs/vue/blob/dev/src/core/observer/array.js#L42 – Johan Commented Dec 3, 2018 at 21:06
-
Ok, thanks. That does not appear in the API docs, so I agree might be dodgy to use (
ob = this.__ob__
maybe it's an internal that could change?) – Richard Matsen Commented Dec 3, 2018 at 22:21 - I just use puted properties in the ponent in lieu of watchers or explicit subscriptions. To get that deep reference on the store item, I would add a store getter that extracts the deep value. – Richard Matsen Commented Dec 3, 2018 at 22:32
- @RichardMatsen Indeed, relying on internals seems like a bad idea :) – Johan Commented Dec 4, 2018 at 9:44
4 Answers
Reset to default 2This API will break the Vuex's data flow which is the core concept of Vuex. The clients would able to mutate/read store state everywhere in Vuex.
Honestly this way is not needed to be implemented in the Vuex since it is just an event emitter. I suggest you to use some event emitter (probably empty Vue instance) in actions.
export const emitter = new Vue()
export default {
// ...
actions: {
// should be called when the store is initialized
// to observe events
observe({ dispatch, mit }) {
emitter.$on('some-event', () => {
mit('someEvent')
})
emitter.$on('other-event', () => {
dispatch('otherEvent')
})
},
// notify some event in action
notify({ state }) {
emitter.$emit('notify', state.someValue)
}
}
}
It solve my problem once when I search in github. Mat be help you. Thanks!!
You can use deepCopy (for example JSON.parse(JSON.stringify())
) to make sure data is reactive
const mutations = {
publish_event(state, payload) {
if(!state.events[payload.key]) {
state.events[payload.key] = { data: payload.data }
} else {
state.events[payload.key] = Object.assign({}, state.events[payload.key], { data: payload.data })
}
state.events = JSON.parse(JSON.stringify(state.events))
}
}
In your ponent above, you're listening for foo_bar
in watcher. Vue watcher only work with ponent data (from data
, puted
or vuex
).
You can redefine your data as ponentData
as below. You can use mapGetters
for shorter syntax:
<script>
import { mapGetters } from 'vuex'
export default {
...mapGetters(['events']),
puted: {
ponentData () {
const eventKeys = ['foo_bar']
return _.reduce(eventKeys, (result, current) => {
result[current] = function() {
return _.get(this, `events.${current}.data`)
}
return result
}, {})
}
},
watch: {
ponentData: function (newVal, oldVal) {
...
}
}
}
</script>
Calling Vue.set
on the object doesn't add observers / reactivity to the data inside the object. This requires an additional Vue.set
Vue.set(state.events, payload.key, {})
Vue.set(state.events[payload.key], 'data', payload.data)
You can also wrap this into a utility function which recursively sets data using Vue.set
Can you try this and let me know if reactivity was triggered in both of these situations?
First, remove this unnecessary wraping with outer object and send payload as simple key/value object with desired event key and data for this key:
{
someKey: 123
}
As second, send some nested data:
{
someKey: {
nested: 'Value'
}
}
But before this, change the mutation code as follows:
const mutations = {
publish_event(state, payload) {
// Instead of the previous code, just "patch"
// the state.events with the payload content.
state.events = { ...state.events, ...payload }
}
}
And do not forget to improve your mapEventGetters function, as data are no more nested in "data" property.
PS: But personally I don't understand why to not use the Vuex with particular getters, because it works, it triggers the reactivity with primitive types:
store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
const state = {
events: {}
}
const actions = {
publish({mit}, payload) {
mit('publish_event', payload)
}
}
const mutations = {
publish_event(state, payload) {
state.events = { ...state.events, ...payload }
}
}
const getters = {
fooBar: state => state.events.fooBar || ''
}
Vue.use(Vuex)
export default new Vuex.Store({
state,
actions,
mutations,
getters
})
main.js
import Vue from 'vue'
import App from '@/App'
import store from '@/store'
new Vue({
store,
render: h => h(App)
}).$mount('main')
some ponent
<template>
<span>{{ fooBar }}</span>
</template>
import { mapGetters, mapActions } from 'vuex'
export default {
name: 'SomeComponent',
puted: {
...mapGetters(['fooBar'])
},
methods: {
...mapActions(['publish'])
},
created () {
setTimeout(() => {
publish({
fooBar: 123
})
}, 3000)
}
}