发布订阅模式的实现

什么叫做“发布-订阅”模式

举一个简单的例子,我对物理感兴趣,于是我上微信公众号关注了“中科院物理所”,所以“中科院物理所”每天发文章我都能收到。
在这个过程中,我“订阅”了中科院物理所的文章,因此,只要它一“发布”文章,我就能收到。在这之间,还有一个媒介——微信公众号平台。我通过这个平台去订阅事件,物理所通过这个平台来发布事件。
发布事件的人,订阅事件的人,加一个两者沟通的中介。这就是一个完整的“发布-订阅”模式。

写一个例子

代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class EventHub {  // 事件中心
constructor() {
this.list = {};
}
publish(eventName) { // 发布
if (!this.list || !this.list.hasOwnProperty(eventName)) {
return;
}
for (let i = 0; i < this.list[eventName].length; i++) {
let fn = this.list[eventName][i].fn;
let event = this.list[eventName][i].event;
fn(event);
}
}
subscribe(eventName, fn) { // 订阅
if (!this.list) {
this.list = {};
}
if (!this.list.hasOwnProperty(eventName)) {
this.list[eventName] = [];
}
let event = new Event(eventName);
this.list[eventName].push({ event: event, fn: fn });
}
}
const eventhub = new EventHub();
eventhub.subscribe("progress", (e) => { // 订阅 progress 事件
console.log(e.type);
});
eventhub.subscribe("progress", () => {
console.log("progress 2");
});
eventhub.publish("progress"); // 发布 progress 事件

从一道面试题说起

通过 new PipeLine() 可以创建一个 pipe 实例,该实例挂载两个方法:

  1. on 方法
    调用 on 方法,可以通过传参来绑定事件名、事件函数,事件函数会默认携带两个参数:ctx、next。
    其中 ctx 是一个对象,访问 name 可以获取当前的事件名。
    next 是一个函数,表示执行事件列表中的下一个事件。如果 next 不被调用,则默认不执行下一个事件。
  2. run 方法
    调用 run 方法,会按照 pipe 的事件绑定顺序,依次执行,直到事件函数中不再调用 next。
    实现效果如下:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    const pipe = new PipeLine();
    pipe.on("start", (ctx, next) =>{
    console.log(ctx.name);
    next();
    });
    pipe.on("progress", (ctx, next) =>{
    console.log(ctx.name);
    });
    pipe.on("end", (ctx, next) =>{
    console.log(ctx.name);
    next();
    });
    pipe.run();
    // 输出结果:"start" "progress"
    // 因为 progress 没有执行 next() , 因此后面绑定的 "end" 不执行

拿到题目的第一反应是发布-订阅模式,与前面的例子不同的是:

  1. 不区分事件名
  2. 默认不继续执行下一个绑定事件

顺着这个思路往下写,应该是先去掉事件名。
因此 this.list[eventName].push() 直接变成了 this.list.push().
这样不论你绑定的是什么事件,都会直接运行,run 方法也不需要传递参数 eventName.
再有就是在 next 方法没有执行的时候阻止后续绑定事件的执行。
也就是把后续执行函数放到 next 方法中传给前一个绑定事件。
原先的代码是这样的

1
2
3
4
5
for (let i = 0; i < this.list.length; i++) {
let fn = this.list[i].fn;
let ctx = this.list[i].ctx;
fn(ctx, next);
}

fn(ctx, next) 提取出来,放到 fn 的第二个参数中,作为 next 函数传递给这个事件,通过递归就可以实现前一个事件调用 next 之后执行下一个事件。

1
2
3
4
5
6
7
8
9
10
let a = (i) => {
if (i < this.list.length) {
let fn = this.list[i].fn;
let ctx = this.list[i].ctx;
fn(ctx, () => {
a(++i);
});
}
};
a(0);

完整代码

如果,我想要默认按顺序执行所有的绑定事件,但是一旦执行了 ctx.stop() 就终止后面的绑定事件呢?
把上面的代码稍稍改一改,让 ctx.stop 终止循环,是不是就可以了?

1
2
3
4
5
6
7
8
9
10
let a = (i) => {
if (i < this.list.length) {
let fn = this.list[i].fn;
let ctx = this.list[i].ctx;
ctx.stop = ()=>{i=this.list.length}
fn(ctx);
a(++i)
}
};
a(0);

看到这里,有没有想到什么?

addEventListener 做了什么

不知道你们有没有了解过 DOM 的 addEventListener 和 click事件。
添加一个事件监听,执行一个点击事件。
下面是一个简单的 click 事件。
源码

1
2
3
4
5
6
7
8
9
10
11
12
let btn = document.querySelector("#btn")
btn.addEventListener("click", (e)=>{
console.log(e.type); // 输出 "click"
})
btn.addEventListener("click", (e)=>{
console.log("2")
e.stopImmediatePropagation(); // 阻止后面绑定的监听事件
})
btn.addEventListener("click", (e)=>{
console.log("3")
})
btn.click() // 手动执行 click 事件

和上面的面试题是不是有点像?
我们把上面面试题代码稍稍改动一下,把 on 变成 addEventListener , 同时把 run 变成 click, 把 ctx 变成 event, 把 stop() 变成 stopImmediatePropagation().
像这样

1
2
3
4
5
6
7
8
9
10
11
12
const pipe = new PipeLine();
pipe.addEventListener("start", (e) => {
console.log(e.type);
});
pipe.addEventListener("progress", (e) => {
console.log(e.type);
e.stopImmediatePropagation()
});
pipe.addEventListener("end", (e) => {
console.log(e.type);
});
pipe.click();

现在是不是更像一点了?
源码
addEventListener 实际上是添加了一个事件监听,在监听这个事件被触发之后,就会执行回调函数。
a.addEventListener("click", fn) a 订阅了 click 事件,一旦这个事件被触发,就调用函数 fn 。
a.click() 发布了 click 事件,通知所有订阅了 click 事件的人,可以执行你的操作了。


以上,发布-订阅模式和实际应用场景。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2018-2023 文初阳
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信