Vue组件间的通信还可借助第三方库来实现。而pubsub-js就是一种实现了消息的订阅与发布的库。使用pubsub-js可以实现任意组件间的通信。

安装pubsub-js

npm i pubsub-js

在需要发布或订阅消息的组件中引入pubsub-js

import pubsub from 'pubsub-js'

发布消息

pubsub-js发布消息可以使用pubsub.publish()方法:

pubsub.publish(msgName, data)
  • msgName:发送消息的名称,字符串类型。
  • data:发送的消息(数据),类型任意。

注:pubsub.publish()$bus.$emit()方法不一样。pubsub.publish()仅有一个data参数作为消息进行发送。而$bus.$emit()的参数数量是可变的,从第2个开始的参数都可作为消息发送。


订阅消息

pubsub-js订阅消息可以使用pubsub.subscribe()方法:

pubsub.subscribe(msgName, callback)
  • msgName:订阅的消息名称,字符串类型。

  • callback:收到消息时,执行的回调函数。

    pubsub.subscribe()的回调函数使用一般的function形式定义时,回调函数中的this指向的是undefined

    而在Vue中定义的回调一般是Lambda表达式。所以使用Lambda表达式直接在pubsub.subscribe()中定义回调函数即可。

  • 返回值:返回当前订阅的ID值。


取消订阅

pubsub-js取消订阅消息可以使用pubsub.unsubscribe()方法:

pubsub.unsubscribe(subId)

subId:订阅的ID值。即,在调用pubsub.subscribe()时返回的ID值。


与全局事件总线的对比

  • 全局事件总线是Vue自带的一个模型,无需引入第三方库。
  • Pubsub是第三方库,其事件的订阅与发布无法在Vue开发者工具中查看。
  • 全局事件总线的功能整体上与消息的订阅发布并无太大差别。

挂载 Pubsub

使用Pubsub时,可以像安装全局事件总线时一样,将Pubsub挂载到Vue实例上,这样在组件中使用Pubsub时就无需多次重复引入Pubsub。

import pubsub from 'pubsub-js'
import Vue from 'vue'
/* ... */

new Vue({
  /* ... */
  beforeCreate() {
    Vue.prototype.$pubsub = pubsub
  },
  /* ... */
}).$mount('#app')
  • 发布消息:

    this.$pubsub.publish(msgName, data)
    

    this指Vue组件实例,下同。

  • 订阅消息:

    const subId = this.$pubsub.subscribe(msgName, callback)
    
  • 取消订阅

    this.$pubsub.unsubscribe(subId)
    

消息订阅发布案例

组件自定义事件中的todo-list案例修改成使用消息订阅与发布实现,并且增加了编辑功能。

注:下方注释内容为...(即<!-- ... -->/* ... */)的部分,代表与原先组件自定义事件中的案例内容相同。

main.js

import Vue from 'vue'
import pubsub from 'pubsub-js'
import App from './App.vue'

Vue.config.productionTip = false

new Vue({
  render: h => h(App),
  beforeCreate() {
    Vue.prototype.$pubsub = pubsub
  },
}).$mount('#app')

todo-list-itme.vue

<template>
<li>
  <label>
    <input type="checkbox" :checked="isCompleted" @change="handleCheck"/>
    <span v-show="!isEdit">{{name}}</span>
    <input 
      v-show="isEdit" 
      type="text"
      v-model.lazy="todoName"
      ref="input"
      @keyup.enter="handleBlur"
      @blur="handleBlur"
    />
  </label>
  <button class="btn btn-danger" @click="handleDelete">删除</button>
  <button 
    v-show="!isEdit"
    class="btn btn-edit" 
    @click="handleEdit"
  >
    编辑
  </button>
</li>
</template>

<script>
export default {
  name: 'todo-list-item',
  props: {/* ... */},
  data() {
    return {
      isEdit: false,
    }
  },
  computed: {
    todoName: {
      get() {
        return this.name
      },
      set(name) {
        if (!name.trim()) return alert('事件名称不能为空!')  // 控制输入不能为空
        this.$pubsub.publish('update-todo', {id: this.id, name})
      }
    },
  },
  methods: {
    // 勾选或取消勾选
    handleCheck() {
      // 通知 App.vue 将对应的 todo 对象的 isCompleted 取反
      this.$pubsub.publish('check-todo', this.id)
    },
    // 删除
    handleDelete() {
      if (confirm(`是否确定删除${this.name}?`)) {
        this.$pubsub.publish('remove-todo', this.id)
      }
    },
    // 进入编辑
    handleEdit() {
      this.isEdit = true
      // 模板重新解析完成后才自动获取焦点
      // $nextTick() 指定的回调,会在下次DOM更新完成之后才执行
      this.$nextTick(() => {
        this.$refs.input.focus()
      })
    },
    // 失去焦点
    handleBlur() {
      this.isEdit = false
    }
  },
}
</script>

<style scoped>
/* ... */
</style>

App.vue

<template>
<div id="root">
  <!-- ... -->
</div>
</template>

<script>
import TodoHeader from './components/todo-header.vue'
import TodoFooter from './components/todo-footer.vue'
import TodoMain from './components/todo-main.vue'

export default {
  name: 'App',
  components: {
    TodoHeader,
    TodoFooter,
    TodoMain,
  },
  data() {
    return {
      // 将 todos 列表定义在 App.vue 中
      todos: JSON.parse(localStorage.getItem('todos')) || [],
      subIds: {},
    }
  },
  watch: {
    todos: {
      deep: true,
      handler(value) {/* ... */},
    },
  },
  computed: {
    // 计算被选 todo 的总数
    completedTotal() {/* ... */},
    // 计算 todos 总数
    total() {/* ... */},
  },
  methods: {
    // 添加一个 todo
    addTodo(todo) {/* ... */},
    // 勾选或取消一个 todo(使用 '_' 作为冗余参数的占位符)
    checkTodo(_, id) {/* ... */},
    // 删除一个 todo(使用 '_' 作为冗余参数的占位符)
    deleteTodo(_, id) {/* ... */},
    // 选择所有或取消选择所有
    checkAllTodo(checked) {
      this.todos.forEach(todo => todo.isCompleted = checked)
    },
    // 清除所有已完成的 todo
    clearAllCompletedTodos() {/* ... */},
  },
  mounted() {
    this.subIds.checkTodo = this.$pubsub
                                .subscribe('check-todo', this.checkTodo)
    this.subIds.removeTodo = this.$pubsub
                                .subscribe('remove-todo', this.deleteTodo)
    this.subIds.updateTodo = this.$pubsub
                                .subscribe('update-todo', this.updateTodo)
  },
  beforeDestroy() {
    // 取消订阅所有消息
    Object.values(this.subIds)
          .forEach(subId => this.$pubsub.unsubscribe(subId))
    this.subIds = {}
  },
}
</script>

<style>
/* ... */
</style>