Vue組件交互之bus模式

一、bus模式簡介

1. bus簡例

bus是一種通過事件實現組件交互的通信模式,它藉助一個額外的Vue實例作爲事件管理中心。任何引入了該Vue實例的組件都處於同一個事件環路內,可以相互註冊和觸發事件。一個簡單的bus實例如下:
bus.js

import Vue from 'vue';

const bus = new Vue();

export default bus;

component1.vue

...
import bus from './bus.js';
bus.$on('move', (payload) => { 
  ... 
})

component2.vue

import bus from './bus.js';
...
bus.$emit('move', payload);

這裏bus是一個空Vue實例,我們在component1和component2中均引入了這個實例,並在component1中註冊了對move事件的監聽。接下來我們在component2的合適時機下觸發move事件,這樣component1就可以接收到這個事件,並觸發對應的回調。

如果bus.$on註冊在組件內部(如methods或各個生命週期函數內),由於箭頭函數內部沒有this,因此你可以在回調函數內直接通過this訪問當前組件實例(如果回調函數是常規函數,可以將組件實例保存在一個局部變量內)。

那麼爲什麼需要這樣一個模式呢?

2. 爲什麼需要bus?

這是因爲,通常來說,在不借助bus模式的情況下,事件的觸發只會發生在父子組件之間,並且只能由子組件向父組件觸發事件。它的大致實現模式如下:
parent.vue

<template>
  <child
    @tick="handleTick"
  ></child>
</template>

<script>
export default {
  methods: {
    handleTick () { ... }
  }
}
</script>

child.vue

...
this.$emit('tick');
...

父組件向子組件綁定了一個對tick事件的監聽,並定義了處理函數,而子函數可以在恰當的時機向父組件觸發該事件。這種模式在封裝第三方組件時極其常用。

另一種不太常見的交互是,由父組件直接調用子組件內的方法:
parent.vue

<template>
  <child ref="child"></child>
</template>

<script>
  export default {
    mounted () {
      this.$refs.child.tick();
    }
  }
</script>

這裏父組件通過ref屬性拿到了對子組件的引用,然後直接調用了子組件內定義的tick方法。當然,真正的函數調用仍然發生在子組件內。這種交互不涉及事件。

上述兩種方法都只能作用於父子組件之間,對於無直接父子關係的組件則束手無策,這也是bus模式的使用背景。

二、bus的應用

1. bus原理

上文的例子中只展示了bus模式的簡單用法,在介紹其他用法之前,我們先通過一張圖來理解何爲bus模式:
在這裏插入圖片描述
由於bus是一個完整的Vue實例,因此它具備事件管理能力。我們在任意組件內導入bus,並通過bus.$on註冊一個事件監聽時,該回調函數就會進入bus內的事件隊列。任何引入了bus的組件,都可以訂閱任何感興趣的事件(即註冊回調事件)。

事件的觸發是同樣的道理,我們只需要在組件內引入bus,然後通過bus.$emit觸發一個事件即可。觸發了一個事件後,事件隊列中的所有回調函數都會按照註冊順序依次執行。

需要注意的是,回調函數的註冊和執行其實都是發生在bus實例上。因此,如果所傳入的回調函數是普通函數,那麼函數內的this將指向bus實例,而不是註冊事件的那個組件實例:

...
bus.$on('move', function(pos){
  this.pos = pos;
})

在這裏插入圖片描述
使用箭頭函數時不會有這個問題,它內部的this指向當前組件實例:

let temp = 123;
bus.$on('move', pos => {
  this.pos = pos;
  console.log(temp);
})

我在這裏特意定義了一個變量temp來說明問題。我們知道,通過作用域鏈,函數內可以訪問外部的變量temp。同樣的,由於箭頭函數不會重新定義this,因此函數內的變量this可以通過作用域鏈拿到外部this。

2. bus的使用

理解了bus模式的原理後,它的使用就非常簡單了。任何需要共享某些事件的組件只需引入同一個bus,就可以註冊和觸發共享的事件:
component1.vue

<template>
  <button @click="handleClick">點擊我</button>
</template>
<script>
import bus from './bus.js';
export default {
  methods: {
    handleClick () {
      bus.$emit('click');
    }
  },
  mounted () {
    bus.$on('move', pos => {
      ...
    });

    bus.$on('tick', payload => {
      ...
    });
  }
}

component2.vue

<template>
  <div @mousemove.native="handleMove"></div>
</template>
<script>
export default {
  methods: {
    handleMove () {
      bus.$emit('move');
    }
  },
  mounted () {
    bus.$on('click', () => {
      ...
    })
  }
}

用一張圖來表示上述關係:
在這裏插入圖片描述
bus允許任意多的組件參與到這個事件環路內,他們共享同一組事件隊列。

另外,如果事件關係很複雜,創建多個互不相關的bus也是可以的:

src
  |-- bus
    |-- clickBus.js
    |-- moveBus.js
    ... 

這裏我們創建了多個bus,clickBus專門用來管理點擊相關的事件,moveBus專門管理與移動相關的事件。需要註冊或觸發相應的事件時,引入對應的bus即可。

總結

一般來說,bus不應該被大規模用於項目中。因爲bus內事件的註冊和觸發分別位於不同的組件內,不便於跟蹤,這在一定程度上會帶來調試上的困難。

從實現原理上來說,Vuex的store模式其實是bus模式的一種封裝和變體,它也是藉助一個額外的Vue實例實現的:
在這裏插入圖片描述
不同的是,bus模式專注於事件管理,而store模式專注於數據管理。

一般來說,Vue推薦開發者多關注業務邏輯(即數據),事件應該由store直接管理,而不是發生在兩個不相關的組件之間。這也是爲什麼Vuex專注於數據管理,而不是事件管理。不過對我們來說,在恰當的時候使用bus模式,卻有可能收到意想不到的效果。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章