簡易使用Vue中的中央事件總線(eventBus)

前言

正如咱們所知Vue中組件的通訊場景不少, 什麼跨多級父子通訊, 兄弟通訊等等。各類各樣的解決方法網上方法都有以及多得飛起, 我就不復制黏貼, 省點ATP。在Vue生態中已經有很好的方案解決各類疑難雜症的通訊問題, 針對中大型項目, 首選Vuex (用過都說好), 可是若是是小型項目使用Vue的eventBus, 是一個不錯的選擇。咱們接下來簡單說下Vue的事件總線的使用和一些常見的問題。vue

使用方法

Vue的實例就是一個事件總線:
    import Vue from 'vue';
    var vm = new Vue({
        mounted () {
            ## 此時就能夠監聽一個自定義事件event1
            this.$on('event1', (data) => {
                consolo.log(data);
            }); 
        },
        methods: {
            trigger () {
                this.$emit('event1', '猴賽雷~');
            }
        }
    });
    vm.trigger(); ## 輸出: 猴賽雷~
    看得出eventBus的使用就是在一個組件內經過$on一個事件, 在一些操做下$emit事件, 這個看起來跟訂閱發佈是一回事。
    可是這裏的eventBus是局部的, 只能在這個組件內用, 若是在一個父組件A監聽了事件, 在B子組件去emit, 是沒有反應的, 
    由於子組件B本身木有監聽該事件。
    <A> 
        <B></B> 
    </A>
    
    A.vue文件:
    export default {
        created () {
            this.$on('你是否是嚶嚶怪', (嚶) => {
                if (嚶.length >= 3) {
                    console.log('你就是嚶嚶怪');
                }
            })
        }
    }
    
    B.vue文件: 
    export default {
        mounted () {
            this.$emit('你是否是嚶嚶怪', '嚶嚶嚶嚶');
        }
    }
複製代碼

實現一個全局的eventBus

全局的eventBus簡單理解爲在一個文件建立一個新的vue實例而後暴露出去, 使用的時候import這個模塊進來便可。咱們來編寫下這個文件:node

在項目中新增一個文件eventBus.js, 代碼實現以下:
import Vue from 'vue';
const Bus = new Vue();
const eventBus = {
    TYPES: { // 'TYPES'
        EVENT1: { // 'TYPES.EVENT1'
            EDIT: { // 'TYPES.EVENT1.EDIT'
                INVOKE: {},
                CANCEL: {}, // 'TYPES.EVENT1.EDIT.CANCEL'
                CONFIRM: {}
            },
            ADD: {
                INVOKE: {},
                CANCEL: {},
                CONFIRM: {}
            },
        },
        EVENT2: {
            EDIT: {
                INVOKE: {},
                CANCEL: {},
                CONFIRM: {}
            },
            DELETE: {
                INVOKE: {},
                CANCEL: {},
                CONFIRM: {}
            },
        }
    },
    // 註冊事件函數
    on (eventType, cb = () => {}) {
        Bus.$on(eventType.toString(), (...args) => {
            cb(...args);
        });
    },
    // 觸發事件函數
    emit (eventType, data) {
        Bus.$emit(eventType.toString(), data);
    },
    // 銷燬註冊事件函數
    off (eventType) {
        Bus.$off(eventType.toString());
    },
    // 註冊事件觸發一次後銷燬函數
    once (eventType, cb = () => {}) {
        Bus.$on(eventType.toString(), (...args) => {
            cb(...args);
            eventBus.off(eventType.toString());
        });
    }
};

(function (typeRoot) {
    /**
     * @param {*} source 要給每一個節點添加鏈的對象
     * @param {*} parentNode 當前節點的鏈 好比 EVENT1.EDIT.CANCEL
     */
    function addNodeChain(source, parentNode = 'TYPES') {
        const isObj = typeof source === 'object';
        if (!isObj) return; // 支持傳入默認的字符串方式
        const separator = !!parentNode ? '.' : '';
        const isObjEmpty = Object.keys(source).length === 0;
        if (isObjEmpty) {
            source['nodeChain'] = parentNode;
            source.toString = function () {
                return parentNode;
            }
            return;
        }
        for (const key in source) {
            if (source.hasOwnProperty(key)) {
                source['nodeChain'] = parentNode;
                source.toString = function () {
                    return parentNode;
                }
                const nodeChain = parentNode + separator + key;
                addNodeChain(source[key], nodeChain);
            }
        }
    }
    addNodeChain(typeRoot);
    Object.freeze(eventBus);
    window.eventBus = eventBus;
})(eventBus.TYPES);

export default eventBus;

複製代碼

上面的定義了一個eventBus對象,裏面定義如下五個屬性:git

  • TYPES (預先定義好的一些事件)
  • on(監聽(訂閱)事件函數)
  • emit(觸發(發佈)事件函數
  • once(只監聽(訂閱)一次事件函數
  • off(移除事件)

TYPES & addNodeChain

上面定義好的eventBus對象很好理解,無非就是簡單封裝了下Bus的一些api, 咱們來講說TYPES對象和addNodeChain方法。
TYPES:
由於on方法第一個傳入的參數是字符串, 也是事件名字, 若是訂閱事件多了, 在項目中必然會存在很多的字符串, 若是不當心寫錯事件名(多寫少些個字母啥的~), Vue並不會給你報錯, 這對錯誤跟蹤和定位都是比較困難的, 所以咱們最好有一個專門的文件來管理和維護這些事件名。這裏使用對象嵌套的方式來事先定義事件名稱, 使用的時候好比 eventBus.on(eventBus.TYPES.EVENT1.UPDATE.INVOKE), 由於事先沒有定義eventBus.TYPES.EVENT1.UPDATE, undefined沒有INVOKE屬性, 控制檯直接報錯, 這樣作的好處在必定狀況避免了上面所說的問題。
github

addNodeChain:
這個方法是遍歷整個對象, 給每一個節點添加nodeChain屬性, 也是當前節點的到根節點的鏈。 而且重寫了當前節點的toString方法, 返回當前節點的nodeChain, 咱們來打印一下執行addNodeChain後eventBus.TYPES的結構。api

{EVENT1: {…}, EVENT2: {…}, nodeChain: "TYPES", toString: ƒ}
EVENT1: {
    ADD: {
        CANCEL: {
        nodeChain: "TYPES.EVENT1.ADD.CANCEL",
        toString: ƒ (),
        },
        CONFIRM: {nodeChain: "TYPES.EVENT1.ADD.CONFIRM", toString: ƒ},
        INVOKE: {nodeChain: "TYPES.EVENT1.ADD.INVOKE", toString: ƒ},
        nodeChain: "TYPES.EVENT1.ADD",
        toString: ƒ (),
    },
    EDIT: {INVOKE: {…}, CANCEL: {…}, CONFIRM: {…}, nodeChain: "TYPES.EVENT1.EDIT", toString: ƒ}
    nodeChain: "TYPES.EVENT1",
    toString: ƒ (),
},
EVENT2: {EDIT: {…}, ADD: {…}, nodeChain: "TYPES.EVENT2", toString: ƒ}
nodeChain: "TYPES",
toString: ƒ (),
__proto__: Object,

而在定義事件方法on時:
   on (eventType, cb = () => {}) {
        Bus.$on(eventType.toString(), (...args) => {
            cb(...args);
        });
    },
上面會把接受進來的參數調用toString方法,傳入給Bus.$on, 所以事件名稱仍是字符串。
至此, 咱們的eventBus就能夠用了。
複製代碼

驗證咱們的eventBus

咱們驗證常見的兩個場景:bash

  • 父子組件通訊
  • 兄弟組件通訊
有如下父組件A, 和子組件B, C, 其中B, C是兄弟組件:
A.vue: 
<template>
    <div>
        <h1>這個是父組件</h1>
        <B></B>
        <C></C>
    </div>
</template>
<script>
import B from './B';
import C from './C';
import eventBus from '@/util/eventBus.js';
export default {
    created () {
        this.eventBus.on(this.eventBus.TYPES.EVENT1.ADD.CONFIRM, (data) => {
            console.log(data);
        })
    },
    components: {
        B,
        C,
    }
}
</script>
--------------------------------------------------------------------------------------------

B.vue:
<template>
    <div>
        <h3>這個是子組件B</h3>
        <button @click="emitParentEvent">點擊觸發父組件A事件</button>
        <button @click="emitBrotherEvent">點擊觸發兄弟組件C事件</button>
    </div>
</template>
<script>
import eventBus from '@/util/eventBus.js';
export default {
    methods: {
        emitParentEvent () {
            eventBus.emit(eventBus.TYPES.EVENT1.ADD.CONFIRM, '父組件A事件被觸發了');
        },
        emitBrotherEvent () {
            eventBus.emit(eventBus.TYPES.EVENT1.ADD.INVOKE, '兄弟組件C事件被觸發了');
        }
    }
}
</script>
--------------------------------------------------------------------------------------------
C.vue:
<template>
    <div>
        <h3>這個是子組件C</h3>
    </div>
</template>
<script>
import eventBus from '@/util/eventBus.js';
export default {
    created () {
        eventBus.on(eventBus.TYPES.EVENT1.ADD.INVOKE, (data) => {
            console.log(data);
        })
    },
}
</script>
複製代碼

咱們在父組件A和子組件C都註冊了事件, 在B組件中經過點擊對應的按鈕emit事件, 當點擊按鈕時控制檯輸出:函數

嗯, 預期符合結果~~~

eventBus的問題

當咱們來回點擊上面切換路由, 從新渲染 (父組件A會從新render), 而後再去點擊B組件的按鈕emit事件, 你會發現, 事件會被重複執行屢次, 好比我切換了6次, 事件被觸發了6次, emmm:ui

緣由是由於在父組件A和子組件C被destory時候, eventBus.$on的事件是不會被銷燬, 組件的每次從新render, 事件就會疊加註冊, 而eventBus是全局的,它不會隨着你頁面切換而從新執行生命週期。
issue: github.com/vuejs/vue/i…
this

尤大對這個問題也做出瞭解析, 如圖:spa

解決屢次觸發的bug

既然eventBus不會隨着組件的銷燬而註銷事件, 那咱們能夠主動去註銷掉事件, 具體的方法就是在eventBus.on的組件中,在beforeDestroy或者 destoryed生命週期中off事件, 咱們來修改一下A.vue和C.vue;

A.vue:
export default {
    beforeDestroy () {

        eventBus.off(eventBus.TYPES.EVENT1.ADD.CONFIRM);
    }
}
-------------------------------------------------------------------------------------
C.vue:
export default {
    beforeDestroy () {
        eventBus.off(eventBus.TYPES.EVENT1.ADD.INVOKE);
    }
}
</script>
複製代碼

修改後, 就不會出現渲染組件, 疊加註冊事件的bug, 每次點擊按鈕只會被emit一次。

尤大建議到可使用mixin注入來處理這個bug, 在mixin組件的鉤子中off註冊的事件

總結

  • 如何實例化一個eventBus和使用方法
  • 編寫一個全局的eventBus和處理事件名的命名的問題
  • 解決中央事件總線eventBus重複執行的bug
相關文章
相關標籤/搜索