Модель публикации-подписки JavaScript

JavaScript

Модель публикации-подписки может показаться незнакомой, но это не так. Часто используется в работе, например методы on и emit в Node.js EventEmitter, методы on и emit в Vue.$onа также$emitметод. Все они используют модель публикации-подписки, чтобы сделать разработку более эффективной и удобной.

1. Что такое модель публикации-подписки

1. Определения

Модель «публикация-подписка» фактически представляет собой зависимость между объектами «один ко многим».. Когда состояние объекта изменяется, все объекты, которые зависят от него, будут уведомлены об изменении состояния.

Подписчики регистрируют события, на которые они хотят подписаться, в диспетчерском центре (Event Channel).Когда издатель (Publisher) публикует событие (Publish Event) в диспетчерском центре, то есть когда событие инициируется, диспетчерский центр Код обработки для абонента единой диспетчерской (пожарного события) зарегистрироваться в диспетчерском центре.

2. Примеры

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

Вышеупомянутая, казалось бы, простая операция на самом деле представляет собой типичную модель публикации-подписки: официальный аккаунт принадлежит издателю, а пользователь — подписчику, пользователь регистрирует событие подписки на официальный аккаунт в диспетчерском центре, а официальный аккаунт выступает в качестве издателя Когда есть новая статья Когда она будет выпущена, официальная учетная запись отправит событие в диспетчерский центр, а диспетчерский центр отправит сообщение, чтобы вовремя проинформировать пользователя.

2. Как реализовать модель публикации-подписки?

1. Идеи реализации

  • создать объект
  • Создать кэш-лист (диспетчерский центр) на этом объекте
  • Метод on используется для добавления функции fn в список кеша (абоненты регистрируют события в диспетчерском центре)
  • Метод emit принимает первое в аргументах как событие и выполняет функцию в соответствующем списке кеша по значению события (издатель публикует событие в диспетчерский центр, а диспетчерский центр обрабатывает код)
  • Метод off может отписаться (unsubscribe) на основе значения события
  • Метод Once слушает только один раз и удаляет кэшированную функцию после вызова (подписываясь один раз)

2. demo1

Давайте взглянем на простую демонстрацию, реализующую методы on и emit, с подробными комментариями в коде.

// 公众号对象
let eventEmitter = {};

// 缓存列表,存放 event 及 fn
eventEmitter.list = {};

// 订阅
eventEmitter.on = function (event, fn) {
    let _this = this;
    // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
    // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
    (_this.list[event] || (_this.list[event] = [])).push(fn);
    return _this;
};

// 发布
eventEmitter.emit = function () {
    let _this = this;
    // 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
    let event = [].shift.call(arguments),
        fns = [..._this.list[event]];
    // 如果缓存列表里没有 fn 就返回 false
    if (!fns || fns.length === 0) {
        return false;
    }
    // 遍历 event 值对应的缓存列表,依次执行 fn
    fns.forEach(fn => {
        fn.apply(_this, arguments);
    });
    return _this;
};

function user1 (content) {
    console.log('用户1订阅了:', content);
};

function user2 (content) {
    console.log('用户2订阅了:', content);
};

// 订阅
eventEmitter.on('article', user1);
eventEmitter.on('article', user2);

// 发布
eventEmitter.emit('article', 'Javascript 发布-订阅模式');

/*
    用户1订阅了: Javascript 发布-订阅模式
    用户2订阅了: Javascript 发布-订阅模式
*/

3. demo2

В этом релизе мы добавили методы Once и Off.

let eventEmitter = {
    // 缓存列表
    list: {},
    // 订阅
    on (event, fn) {
        let _this = this;
        // 如果对象中没有对应的 event 值,也就是说明没有订阅过,就给 event 创建个缓存列表
        // 如有对象中有相应的 event 值,把 fn 添加到对应 event 的缓存列表里
        (_this.list[event] || (_this.list[event] = [])).push(fn);
        return _this;
    },
    // 监听一次
    once (event, fn) {
        // 先绑定,调用后删除
        let _this = this;
        function on () {
            _this.off(event, on);
            fn.apply(_this, arguments);
        }
        on.fn = fn;
        _this.on(event, on);
        return _this;
    },
    // 取消订阅
    off (event, fn) {
        let _this = this;
        let fns = _this.list[event];
        // 如果缓存列表中没有相应的 fn,返回false
        if (!fns) return false;
        if (!fn) {
            // 如果没有传 fn 的话,就会将 event 值对应缓存列表中的 fn 都清空
            fns && (fns.length = 0);
        } else {
            // 若有 fn,遍历缓存列表,看看传入的 fn 与哪个函数相同,如果相同就直接从缓存列表中删掉即可
            let cb;
            for (let i = 0, cbLen = fns.length; i < cbLen; i++) {
                cb = fns[i];
                if (cb === fn || cb.fn === fn) {
                    fns.splice(i, 1);
                    break
                }
            }
        }
        return _this;
    },
    // 发布
    emit () {
        let _this = this;
        // 第一个参数是对应的 event 值,直接用数组的 shift 方法取出
        let event = [].shift.call(arguments),
            fns = [..._this.list[event]];
        // 如果缓存列表里没有 fn 就返回 false
        if (!fns || fns.length === 0) {
            return false;
        }
        // 遍历 event 值对应的缓存列表,依次执行 fn
        fns.forEach(fn => {
            fn.apply(_this, arguments);
        });
        return _this;
    }
};

function user1 (content) {
    console.log('用户1订阅了:', content);
}

function user2 (content) {
    console.log('用户2订阅了:', content);
}

function user3 (content) {
    console.log('用户3订阅了:', content);
}

function user4 (content) {
    console.log('用户4订阅了:', content);
}

// 订阅
eventEmitter.on('article1', user1);
eventEmitter.on('article1', user2);
eventEmitter.on('article1', user3);

// 取消user2方法的订阅
eventEmitter.off('article1', user2);

eventEmitter.once('article2', user4)

// 发布
eventEmitter.emit('article1', 'Javascript 发布-订阅模式');
eventEmitter.emit('article1', 'Javascript 发布-订阅模式');
eventEmitter.emit('article2', 'Javascript 观察者模式');
eventEmitter.emit('article2', 'Javascript 观察者模式');

// eventEmitter.on('article1', user3).emit('article1', 'test111');

/*
    用户1订阅了: Javascript 发布-订阅模式
    用户3订阅了: Javascript 发布-订阅模式
    用户1订阅了: Javascript 发布-订阅模式
    用户3订阅了: Javascript 发布-订阅模式
    用户4订阅了: Javascript 观察者模式
*/

3. Реализация во Vue

Зная модель публикации-подписки, давайте посмотрим, как реализовать ее в Vue.$onа также$emitметод, посмотрите непосредственно на исходный код:

function eventsMixin (Vue) {
    var hookRE = /^hook:/;
    Vue.prototype.$on = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // event 为数组时,循环执行 $on
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$on(event[i], fn);
            }
        } else {
            (vm._events[event] || (vm._events[event] = [])).push(fn);
            // optimize hook:event cost by using a boolean flag marked at registration 
            // instead of a hash lookup
            if (hookRE.test(event)) {
                vm._hasHookEvent = true;
            }
        }
        return vm
    };

    Vue.prototype.$once = function (event, fn) {
        var vm = this;
        // 先绑定,后删除
        function on () {
        	vm.$off(event, on);
            fn.apply(vm, arguments);
        }
        on.fn = fn;
        vm.$on(event, on);
        return vm
    };

    Vue.prototype.$off = function (event, fn) {
        var this$1 = this;

        var vm = this;
        // all,若没有传参数,清空所有订阅
        if (!arguments.length) {
            vm._events = Object.create(null);
            return vm
        }
        // array of events,events 为数组时,循环执行 $off
        if (Array.isArray(event)) {
            for (var i = 0, l = event.length; i < l; i++) {
                this$1.$off(event[i], fn);
            }
            return vm
        }
        // specific event
        var cbs = vm._events[event];
        if (!cbs) {
        	// 没有 cbs 直接 return this
            return vm
        }
        if (!fn) {
        	// 若没有 handler,清空 event 对应的缓存列表
            vm._events[event] = null;
            return vm
        }
        if (fn) {
            // specific handler,删除相应的 handler
            var cb;
            var i$1 = cbs.length;
            while (i$1--) {
                cb = cbs[i$1];
                if (cb === fn || cb.fn === fn) {
                    cbs.splice(i$1, 1);
                    break
                }
            }
        }
        return vm
    };

    Vue.prototype.$emit = function (event) {
        var vm = this;
        {
        	// 传入的 event 区分大小写,若不一致,有提示
            var lowerCaseEvent = event.toLowerCase();
            if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
                tip(
                    "Event \"" + lowerCaseEvent + "\" is emitted in component " +
                    (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
                    "Note that HTML attributes are case-insensitive and you cannot use " +
                    "v-on to listen to camelCase events when using in-DOM templates. " +
                    "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
                );
            }
        }
        var cbs = vm._events[event];
        if (cbs) {
            cbs = cbs.length > 1 ? toArray(cbs) : cbs;
            // 只取回调函数,不取 event
            var args = toArray(arguments, 1);
            for (var i = 0, l = cbs.length; i < l; i++) {
                try {
                    cbs[i].apply(vm, args);
                } catch (e) {
                    handleError(e, vm, ("event handler for \"" + event + "\""));
                }
            }
        }
        return vm
    };
}

/***
   * Convert an Array-like object to a real Array.
   */
function toArray (list, start) {
    start = start || 0;
    var i = list.length - start;
    var ret = new Array(i);
    while (i--) {
      	ret[i] = list[i + start];
    }
    return ret
}

Идеи реализации в основном такие же, как и в первом пункте второго пункта выше: идеи реализации. Метод, реализованный в Vue, поддерживает подписку на события массива.

4. Резюме

1. Преимущества

  • Развязка между объектами
  • В асинхронном программировании можно написать более слабосвязанный код.

2. Недостатки

  • Само создание подписчика занимает определенное количество времени и памяти
  • Хотя связь между объектами может быть ослаблена, когда несколько издателей и подписчиков вложены друг в друга, программу трудно отслеживать и поддерживать.

5. Расширение (разница между режимом публикации-подписки и режимом наблюдателя)

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

Прямо над:

观察者模式: Наблюдатель (Observer) напрямую подписывается (Subscribe) субъекта (Subject), и когда субъект активируется, он запускает событие (Fire Event) в наблюдателе.

发布订阅模式: Абонент регистрирует (Subscribe) событие, на которое он хочет подписаться, в диспетчерском центре (Event Channel), когда издатель (Publisher) публикует событие (Publish Event) в диспетчерском центре, то есть при срабатывании события диспетчер Абонент центральной единой диспетчерской (Пожарное событие) регистрирует код обработки в диспетчерском центре.

差异:

  • В шаблоне наблюдатель наблюдатель знает о субъекте, а субъект ведет учет наблюдателя. Однако в модели публикации-подписки издатель и подписчик не знают о существовании друг друга. Они общаются только через брокера сообщений.

  • В шаблоне публикации-подписки компоненты слабо связаны, в отличие от шаблона наблюдателя.

  • Паттерн наблюдателя в основном синхронный, например, когда происходит событие, субъект вызывает метод наблюдателя. Принимая во внимание, что шаблон публикации-подписки в основном асинхронный (с использованием очередей сообщений).

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