最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

javascript - Global event bus using Vuex - always notify subscribers - Stack Overflow

programmeradmin0浏览0评论

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
 |  Show 7 more ments

4 Answers 4

Reset to default 2

This 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)
  }
}
发布评论

评论列表(0)

  1. 暂无评论