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

安装pubsub-js

1npm i pubsub-js

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

1import pubsub from 'pubsub-js'

发布消息

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

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

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


订阅消息

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

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

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

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

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

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


取消订阅

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

1pubsub.unsubscribe(subId)

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


与全局事件总线的对比

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

挂载 Pubsub

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

 1import pubsub from 'pubsub-js'
 2import Vue from 'vue'
 3/* ... */
 4
 5new Vue({
 6  /* ... */
 7  beforeCreate() {
 8    Vue.prototype.$pubsub = pubsub
 9  },
10  /* ... */
11}).$mount('#app')
  • 发布消息:

    1this.$pubsub.publish(msgName, data)
    

    this指Vue组件实例,下同。

  • 订阅消息:

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

    1this.$pubsub.unsubscribe(subId)
    

消息订阅发布案例

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

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

main.js

 1import Vue from 'vue'
 2import pubsub from 'pubsub-js'
 3import App from './App.vue'
 4
 5Vue.config.productionTip = false
 6
 7new Vue({
 8  render: h => h(App),
 9  beforeCreate() {
10    Vue.prototype.$pubsub = pubsub
11  },
12}).$mount('#app')

todo-list-itme.vue

 1<template>
 2<li>
 3  <label>
 4    <input type="checkbox" :checked="isCompleted" @change="handleCheck"/>
 5    <span v-show="!isEdit">{{name}}</span>
 6    <input 
 7      v-show="isEdit" 
 8      type="text"
 9      v-model.lazy="todoName"
10      ref="input"
11      @keyup.enter="handleBlur"
12      @blur="handleBlur"
13    />
14  </label>
15  <button class="btn btn-danger" @click="handleDelete">删除</button>
16  <button 
17    v-show="!isEdit"
18    class="btn btn-edit" 
19    @click="handleEdit"
20  >
21    编辑
22  </button>
23</li>
24</template>
25
26<script>
27export default {
28  name: 'todo-list-item',
29  props: {/* ... */},
30  data() {
31    return {
32      isEdit: false,
33    }
34  },
35  computed: {
36    todoName: {
37      get() {
38        return this.name
39      },
40      set(name) {
41        if (!name.trim()) return alert('事件名称不能为空!')  // 控制输入不能为空
42        this.$pubsub.publish('update-todo', {id: this.id, name})
43      }
44    },
45  },
46  methods: {
47    // 勾选或取消勾选
48    handleCheck() {
49      // 通知 App.vue 将对应的 todo 对象的 isCompleted 取反
50      this.$pubsub.publish('check-todo', this.id)
51    },
52    // 删除
53    handleDelete() {
54      if (confirm(`是否确定删除${this.name}?`)) {
55        this.$pubsub.publish('remove-todo', this.id)
56      }
57    },
58    // 进入编辑
59    handleEdit() {
60      this.isEdit = true
61      // 模板重新解析完成后才自动获取焦点
62      // $nextTick() 指定的回调,会在下次DOM更新完成之后才执行
63      this.$nextTick(() => {
64        this.$refs.input.focus()
65      })
66    },
67    // 失去焦点
68    handleBlur() {
69      this.isEdit = false
70    }
71  },
72}
73</script>
74
75<style scoped>
76/* ... */
77</style>

App.vue

 1<template>
 2<div id="root">
 3  <!-- ... -->
 4</div>
 5</template>
 6
 7<script>
 8import TodoHeader from './components/todo-header.vue'
 9import TodoFooter from './components/todo-footer.vue'
10import TodoMain from './components/todo-main.vue'
11
12export default {
13  name: 'App',
14  components: {
15    TodoHeader,
16    TodoFooter,
17    TodoMain,
18  },
19  data() {
20    return {
21      // 将 todos 列表定义在 App.vue 中
22      todos: JSON.parse(localStorage.getItem('todos')) || [],
23      subIds: {},
24    }
25  },
26  watch: {
27    todos: {
28      deep: true,
29      handler(value) {/* ... */},
30    },
31  },
32  computed: {
33    // 计算被选 todo 的总数
34    completedTotal() {/* ... */},
35    // 计算 todos 总数
36    total() {/* ... */},
37  },
38  methods: {
39    // 添加一个 todo
40    addTodo(todo) {/* ... */},
41    // 勾选或取消一个 todo(使用 '_' 作为冗余参数的占位符)
42    checkTodo(_, id) {/* ... */},
43    // 删除一个 todo(使用 '_' 作为冗余参数的占位符)
44    deleteTodo(_, id) {/* ... */},
45    // 选择所有或取消选择所有
46    checkAllTodo(checked) {
47      this.todos.forEach(todo => todo.isCompleted = checked)
48    },
49    // 清除所有已完成的 todo
50    clearAllCompletedTodos() {/* ... */},
51  },
52  mounted() {
53    this.subIds.checkTodo = this.$pubsub
54                                .subscribe('check-todo', this.checkTodo)
55    this.subIds.removeTodo = this.$pubsub
56                                .subscribe('remove-todo', this.deleteTodo)
57    this.subIds.updateTodo = this.$pubsub
58                                .subscribe('update-todo', this.updateTodo)
59  },
60  beforeDestroy() {
61    // 取消订阅所有消息
62    Object.values(this.subIds)
63          .forEach(subId => this.$pubsub.unsubscribe(subId))
64    this.subIds = {}
65  },
66}
67</script>
68
69<style>
70/* ... */
71</style>