Использование шины событий в Vue

JavaScript Vue.js
Использование шины событий в Vue

задний план

Существует множество методов управления состоянием Vue. Существуют различные варианты межуровневого взаимодействия родитель-потомок, братское общение и т. д. В экосистеме Vue уже есть хорошие решения для решения коммуникационных проблем в различных сценариях.Для среднего и крупного управления статусом проекта первый выборVuex, но если это небольшой проект с использованием VueeventBus, это хороший выбор без дополнительных зависимостей. Давайте кратко поговорим об использовании EventBus Vue и некоторых распространенных проблемах.

Публикация подписки в Vue

Vue реализует набор публикаций по подписке на прототипе, поэтому мы можем использовать$emit、$onпричина.

import Vue from 'vue';
var vm = new Vue({
    mounted () {
        // 此时就可以监听一个自定义事件event1
        this.$on('event1', (data) => {
            consolo.log(data);
        }); 
    },
    methods: {
        trigger () {
            this.$emit('event1', '猴赛雷~');
        }
    }
});

Принцип публикации подписки

Давайте сначала разберемся с принципом реализации публикации по подписке:

class EventBus {
    eventMap = new Map;
    on(eventName, handler) {
        let eventHandlers = this.eventMap.get(eventName);
        if (!eventHandlers) {
            eventHandlers = [];
            this.eventMap.set(eventName, eventHandlers);
        } 
        eventHandlers.push(handler);
    }
    emit(eventName, ...args) {
        const eventHandlers = this.eventMap.get(eventName) || [];
        eventHandlers.forEach(handler => handler(...args));
    }
    once(eventName, handler) {
        const onceHandler = (...args) => {
            this.off(eventName, onceHandler);
            handler(...args);
        }
        this.on(eventName, onceHandler);
    }
    off(eventName, handler) {
        let eventHandlers = this.eventMap.get(eventName);
        if (handler) {
            if (eventHandlers) {
                const idx = eventHandlers.findIndex(cb => cb === handler)
                idx > - 1 && eventHandlers.splice(idx, 1);
            }
        } else {
            this.eventMap.set(eventName, [])
        }
    }
}

const ev = new EventBus();

ev.on('123', console.log);
ev.once('1234', console.log);
ev.emit('123', 1); // 1
ev.emit('123', 2); // 1
ev.emit('1234', 3); // 3
ev.emit('1234', 4); // do nothing

На события можно подписываться и публиковать в нужное время в рамках одного компонента. Но следующий сценарий недопустим, так как система публикации подписки в компоненте является частной, и компоненты не мешают друг другу. Но мы можем использовать глобальную систему публикации подписки, которую можно использовать в любом компоненте.

недействительный $ emit.:

<A> 
    <B></B> 
</A>

// A.vue文件:
export default {
    created () {
        this.$on('a-event', () => { // do something})
    }
}

// B.vue文件: 
export default {
    mounted () {
        this.$emit('a-event', 'come from B.vue');
    }
}

Реализовать глобальную шину событий

Глобальный eventBus просто понимается как создание нового экземпляра vue в файле, а затем его предоставление.При его использовании вы можете импортировать этот модуль. Напишем этот файл:

在项目中新增一个文件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, который определяет следующие пять свойств:

  • TYPES (预先定义好的一些事件模型)
  • on(监听(订阅)事件函数)
  • emit(触发(发布)事件函数
  • once(只监听(订阅)一次事件函数
  • off(移除事件)

TYPES & addNodeChain

определено вышеeventBusОбъект хорошо изучен, это не что иное, как простая инкапсуляция какого-то аписа Шина, поговорим об этомTYPESобъект иaddNodeChainметод.
TYPES:
Поскольку первым параметром, передаваемым методом on, является строка, которая также является именем события, у использования строки в качестве имени события есть недостаток: если событие написано неправильно, Vue не выдаст ошибку.(vuex会提示dispatch或者commit不存在), что затрудняет отслеживание и локализацию ошибок, поэтому мы можем сохранять имена этих событий в переменной (на самом деле это события или строки). Здесь для предварительного определения имени события используется метод вложения объектов, при его использовании, напримерeventBus.on(eventBus.TYPES.EVENT1.UPDATE.INVOKE), потому что нет предварительного определенияeventBus.TYPES.EVENT1.UPDATE, undefinedнетINVOKEатрибут, консоль напрямую сообщает об ошибке, поэтому естественно знать, что имя события неверно.

addNodeChain:
Этот метод заключается в обходе всего объекта и добавлении к каждому узлуnodeChainСвойства, но и текущий узел к корневому узлу цепочки. ToString и перезаписывает текущий узел, текущий узел возвращаетnodeChain, напечатаем исполнениеaddNodeChainСтруктура после eventBus.TYPES.

{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: ƒ (),
    },
},

而在定义事件方法on时:
   on (eventType, cb = () => {}) {
        Bus.$on(eventType.toString(), (...args) => {
            cb(...args);
        });
    },
上面会把接受进来的参数调用toString方法,传入给Bus.$on, 因此事件名称还是字符串。
至此, 我们的eventBus就可以用了。

тестовая шина событий

Мы проверяем два распространенных сценария:

  • Взаимодействие компонентов родитель-потомок
  • Взаимодействие компонентов родственного компонента
有以下父组件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 () {
        eventBus.on(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 нажмите соответствующую кнопку, чтобы создать событие.При нажатии кнопки консоль выводит:

Что ж, ожидание соответствует результату~~~

Проблема с EventBus

Когда мы нажимаем назад и вперед вышепереключить маршрут, перерендерить(Родительский компонент A будет повторно визуализирован), а затем нажмите кнопку события компонента B, вы обнаружите, что событие будет повторяться много раз, например, если я переключаюсь 6 раз, событие запускается 6 раз, эммм :

Причина в том, что когда родительский компонент A и дочерний компонент C уничтожаются, событие eventBus.$on не будет уничтожено.Каждый раз, когда компонент перерисовывается, событие будет накладываться и регистрироваться, а eventBus является глобальным, это не последует. Вы переключаете страницы и повторно выполняете жизненный цикл.

issue: GitHub.com/vUEJS/vUE/i…

You Da также сделал анализ этой проблемы, как показано на рисунке:

Решите ошибку, которая срабатывает несколько раз

теперь, когдаeventBusСобытие не будет отменено при уничтожении компонента, тогда мы можем взять на себя инициативу по отмене события, конкретный метод находится вeventBus.onкомпоненты, вbeforeDestroyилиdestoryedв жизненном циклеoffсобытие, давайте изменим A.vue;

A.vue:
export default {
    created () {
        eventBus.on(eventBus.TYPES.EVENT1.ADD.CONFIRM, data => {
            console.log(data);
        })
        this.$once('hook:beforeDestroy', () =>  {
            eventBus.off(eventBus.TYPES.EVENT1.ADD.CONFIRM);
        })
    }
    
}
</script>

То же самое относится и к компоненту C.

После модификации компонент рендеринга не появится, а ошибка события наложенной регистрации будет генерироваться только один раз при каждом нажатии кнопки.

Суммировать

  • Как создать экземпляр eventBus и использовать методы
  • Напишите глобальную шину событий и обработайте присвоение имен событиям.
  • Устранена ошибка повторного выполнения центральной шины событий eventBus