amandakelake/blog

订阅-发布模式

amandakelake opened this issue · 0 comments

初学者都会用的订阅-发布模式

只要我们操作过DOM,都已经使用过订阅发布模式了,即使初学那会并不知道我们用了这种模式,看下这段代码:

document.getElementById("myId").addEventListener('click', () => {
  //... do something
})

只要用户点击了id是myId的DOM元素,就会自动执行注册的匿名函数

这里相当于订阅了这个DOM节点的click事件,然后等待用户发布事件

当然,我们也可以随意的增加多个订阅者,或者删除订阅者

const ele = document.getElementById("myId");
ele.addEventListener('click', () => alert('hi'));
ele.addEventListener('click', () => alert('hello'));
ele.addEventListener('touchmove', () => alert('world'));

// 注意,通过 addEventListener() 来添加的匿名函数无法移除
ele.removeEventListener('click', handler);

订阅-发布的简单实现

除了常见的DOM事件,在日常业务开发中,我们经常会碰到需要实现自定义事件的场景,或者我们常用的vue框架也是利用订阅发布模式来实现的

下面我们一步步来实现一个简单的订阅发布模式,以下是核心步骤

  • 创建一个全局对象作为发布者,发布者需要维护一个缓存列表hub,用于存放回调函数
  • 实现订阅功能on,订阅事件,并注册回调函数,保存到发布者的缓存列表中
  • 实现发布功能emit,发布事件,执行对应事件的回调函数
  • 实现取消订阅功能off
export const createEventHub = () => ({
  // 生成一个空对象,该对象不继承Object.prototype的属性,比如toString这些它都不需要
  // hub的格式: { [event1]: [handler1, handler2, handler3, ...], [event2]: [], [event3]: [], ...}
  hub: Object.create(null),
  // 订阅事件,并注册回调函数handler
  on(event, handler) {
    // 如果事件不存在,创建一个event空数组
    if (!this.hub[event]) {
      this.hub[event] = [];
    }
    this.hub[event].push(handler);
  },
  // 发布事件
  emit(event, data) {
    (this.hub[event] || []).forEach(handler => handler(data));
  },
  // 取消订阅
  off(event, handler) {
    // 寻找hub[event]里是否存在这个handler
    const i = (this.hub[event] || []).findIndex(h => h === handler);
    if (i > -1) {
      // 如果存在,移除该事件所绑定的handler
      this.hub[event].splice(i, 1);
    }
  }
})

使用方式

const eventHub = createEventHub();
let createEventHubVariable = 1;

const handler = data => {
  console.log(data);
}
// hub订阅了myEvent, myEvent2 两个事件
// 其中myEvent事件注册了两个回调handler 和 匿名函数
eventHub.on('myEvent', handler);
eventHub.on('myEvent', () => {
  console.log('I am handler 2');
  console.log('eventHub', eventHub)
});
eventHub.on('myEvent2', () => {
  createEventHubVariable++;
  console.log('eventHub', eventHub);
});

// 某些用户或者数据变化产生的交互:发布
// 传入不同的参数,执行回调handler, 同时也会执行上面注册的匿名函数
eventHub.emit('myEvent', 'I am a string');
eventHub.emit('myEvent', { arg: 'I am a object'});
eventHub.emit('myEvent2');

// 只移除了handler, 匿名函数还是在的
eventHub.off('myEvent', handler)

Demo

下面的完整代码都可以在这里找到codepen: 订阅-发布模式, 自己随意改改代码就知道来龙去脉了

dec879a8-6948-4ea1-9b9d-0ae4acc0f6ea

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>FE-environment</title>
  <style>
    button {
      background-color: #e6e6e6;
      padding: 5px 10px;
      border-radius: 2px;
      margin: 5px 10px;
    }
  </style>
</head>

<body>
  <h1>FE-environment</h1>
  <div>发布订阅简易版本</div>
  <div>
    <button id="log-hub">log hub</button>
    <button id="log-va">log变量</button>
  </div>
  <div>
    <span>变量createEventHubVariable</span>
    <span id="createEventHubVariable"></span>
  </div>
  <div>
    <button id="subscribe-myEvent-h">订阅myEvent handler</button>
    <button id="subscribe-myEvent-u">订阅myEvent 匿名函数</button>
    <button id="subscribe-myEvent2">订阅myEvent2</button>
  </div>
  <div>
    <button id="emit-myEvent">发布myEvent</button>
    <button id="emit-myEvent2">发布myEvent2</button>
  </div>
  <div>
    <button id="off-myEvent-handler">移除myEvent handler</button>
  </div>

</body>

</html>
   button {
      background-color: #e6e6e6;
      padding: 5px 10px;
      border-radius: 2px;
      margin: 5px 10px;
    }
const createEventHub = () => ({
  // 生成一个空对象,该对象不继承Object.prototype的属性,比如toString这些它都不需要
  // hub的格式: { [event1]: [handler1, handler2, handler3, ...], [event2]: [], [event3]: [], ...}
  hub: Object.create(null),
  // 订阅事件,并注册回调函数handler
  on(event, handler) {
    // 如果事件不存在,创建一个event空数组
    if (!this.hub[event]) {
      this.hub[event] = [];
    }
    this.hub[event].push(handler);
  },
  // 发布事件
  emit(event, data) {
    (this.hub[event] || []).forEach(handler => handler(data));
  },
  // 取消订阅
  off(event, handler) {
    // 寻找hub[event]里是否存在这个handler
    const i = (this.hub[event] || []).findIndex(h => h === handler);
    if (i > -1) {
      // 如果存在,移除该事件所绑定的handler
      this.hub[event].splice(i, 1);
    }
  }
})
const eventHub = createEventHub();

let createEventHubVariable = 1;
document.getElementById("createEventHubVariable").innerHTML = createEventHubVariable;

const handler = data => {
  console.log(data);
}
/*
// hub订阅了myEvent, myEvent2 两个事件
// 其中myEvent事件注册了两个回调handler 和 匿名函数
eventHub.on('myEvent', handler);
eventHub.on('myEvent', () => {
  console.log('I am handler 2');
  console.log('eventHub', eventHub)
});
eventHub.on('myEvent2', () => {
  createEventHubVariable++;
  console.log('eventHub', eventHub);
});

// 某些用户或者数据变化产生的交互:发布
// 传入不同的参数,执行回调handler, 同时也会执行上面注册的匿名函数
eventHub.emit('myEvent', 'I am a string');
eventHub.emit('myEvent', { arg: 'I am a object'});
eventHub.emit('myEvent2');

// 只移除了handler, 匿名函数还是在的
eventHub.off('myEvent', handler)
*/
document.getElementById("log-hub").addEventListener('click', () => {
  console.log('eventHub', eventHub);
})

document.getElementById("log-va").addEventListener('click', () => {
  console.log('createEventHubVariable', createEventHubVariable);
})

document.getElementById("subscribe-myEvent-h").addEventListener('click', () => {
  eventHub.on('myEvent', handler);
  console.log('eventHub', eventHub);
})

document.getElementById("subscribe-myEvent-u").addEventListener('click', () => {
  eventHub.on('myEvent', () => console.log('I am handler 2'));
  console.log('eventHub', eventHub)
})

document.getElementById("subscribe-myEvent2").addEventListener('click', () => {
  eventHub.on('myEvent2', () => {
    createEventHubVariable++;
    document.getElementById("createEventHubVariable").innerHTML = createEventHubVariable;
  });
  console.log('eventHub', eventHub)
})

document.getElementById("emit-myEvent").addEventListener('click', () => {
  eventHub.emit('myEvent', 'I am a string');
  eventHub.emit('myEvent', {
    arg: 'I am a object'
  });
})

document.getElementById("emit-myEvent2").addEventListener('click', () => {
  eventHub.emit('myEvent2');
  document.getElementById("createEventHubVariable").innerHTML = createEventHubVariable;
  console.log('createEventHubVariable', createEventHubVariable);
})

document.getElementById("off-myEvent-handler").addEventListener('click', () => {
  eventHub.off('myEvent', handler);
  console.log('eventHub', eventHub);
})