手写发布订阅

通信的中介

第一核心代码:原始

const eventHub = {
on: ()=>{},//
emit:()=>{},
off:()=>{}
}

eventHub.on('click',f1)//在任意时刻用on来监听一个事件;
eventHub.off('click',f1)//不想监听,监听click的时候,调用f1了。
setTimeout(()=>{
eventHub.emit('click','frank')
},3000)//3秒后在用户点击什么,来触发这个函数
复制代码

第二:关注输入和输出

  • 写的是一个函数,只需要关注他的输入和输出了。name是事件的名字
  • on接收两个参数,name,fn,输出值没人关心,就return undefined;
  • off接收两个参数,name,fn,输出值没人关心,就return undefined;
  • emit接收两个参数name,data,输出值没人关心,就return undefined;
const eventHub = {
on: (name,fn)=>{
return undefined
},
emit:(name.data)=>{
return undefined
},
off:(name,fn)=>{
return undefined
  }
}

eventHub.on('click',f1)//在任意时刻用on来监听一个事件;
eventHub.off('click',f1)//不想监听,监听click的时候,调用f1了。
setTimeout(()=>{
eventHub.emit('click','frank')
},3000)//3秒后在用户点击什么,来触发这个函数
复制代码

上面就是核心的函数,已经写完了

实现功能

  • on的功能是,如果监听一个事件,就把他放到任务队列里面;
  • 任务就是函数,队列,就是监听事件时,有可能调用多个函数,先进先出。
  • 先提供个queue:[[]],不同的name,有不同的数组,叫映射,所以用map,此处就用queueMap:{},
  • 映射就是用map,也就是哈希表,queuemap队列的表;
  • 把on监听的事件放到map里面,eventHub.map[name].push(fn),如果是空的就给他初始化一下,如果是空的,就让他等于初始化的队列吧
  • `if(eventHub.map[name]===undefined){

eventHub.map[name] = []
}`

  • 但上面这样写不好看,可以替换成下面eventHub.map[name] = undefined || [],如果为空就是后面的那个空数组。
  • 初始化之后就是push

第三个步骤:on的时候添加具体事件

  • 就是on的时候,找到一个对列,队列里面放一个任务;
const eventHub = {
map:{},
on: (name,fn)=>{
eventHub.map[name] = undefined || []
eventHub.map[name].push(fn)
},
复制代码

第4个步骤,off的应用

  • on需要初始化,off不需要初始化,如果是空的直接返回

if(!eventHub.map[name]){return}

  • 接着去找里面有没有这个函数;const index = eventHub.map[name].indexOf(fn)
  • 如果if(index<0),就直接return;
  • if (index>=0)说明把fn添加到队列里面了,那就把它删掉;
  • 如何删除eventHub.map[name].splice(index,1)
off:(name,fn)=>{
if(!eventHub.map[name]){return}
const index = eventHub.map[name].indexOf(fn)
if(index<0){return}
eventHub.map[name].splice(index,1)
  }
复制代码
  • 上面代码中eventHub.map[name]重复了太多次,用缩短法alias,用一个字母代替。q,最好只做读的操作,不做写的操作,所以在on里面不能用。
off:(name,fn)=>{
const q = eventHub.map[name]
if(!q){return}
const index = q.indexOf(fn)
if(index<0){return}
q.splice(index,1)
  }
复制代码

第5步:emit的操作

  • emit的作用就是把队列里面的内容调用一下;
    • 首先找到这个队列;const q = eventHub.mao[name]
    • 如果里面的内容是空的,什么都不做if(!q) return,短路法;
    • 到了下一步,就证明这个q不为空了;
    • 不为空就把Q遍历一下,forEach和map都可以遍历,指示map有返回值,q.map(f=>f(data));对里面每一个f进行调用;
    • 写的时候可以这样q.map(f=>f.call(null,data))
emit:(name,data)=>{
const q = eventHub.map[name]
if(!q) return
q.map(f=>f.call(null,data))
return undefined
}
复制代码

来个大综合

const eventHub = {
map:{
},
on:(name,fn)=>{
eventHub.map[name] = eventHub.map[name] || []
eventHub.map[name].push(fn)
},
emit:(name,data)=>{
const q = eventHub.map[name]
if(!q) return
q.map(f => f.call(undefined,data))
return undefined
},
off:(name,fn)=>{
const q = eventHub.map[name]
if(!q){return}
const index = q.indexOf(fn)
if(index<0){return}
q.splice(index,1)
}
}

eventHub.on('click',console.log)//在任意时刻用on来监听一个事件;
eventHub.on('click',console.error)
setTimeout(()=>{
eventHub.emit('click','frank')
},3000)//3秒后在用户点击什么,来触发这个函数
复制代码
 const eventHub = { 
 map: { // click: [f1 , f2] }, 
 on: (name, fn)=>{ 
 eventHub.map[name] = eventHub.map[name] || []eventHub.map[name].push(fn)
 },emit: (name, data)=>{
 const q = eventHub.map[name] if(!q) return q.map(f => f.call(null, data)) return undefined 
 }, 
 off: (name, fn)=>{
 const q = eventHub.map[name]
 if(!q){ return }
 const index = q.indexOf(fn)
 if(index < 0) {return } 
 q.splice(index, 1) } 
 } 
 eventHub.on('click', console.log)
 eventHub.on('click', console.error) 
 setTimeout(()=>{ eventHub.emit('click', 'frank') },3000)

复制代码

定义

发布-订阅模式其实是一种对象间一对多的依赖关系,当一个对象的状态发送改变时,所有依赖于它的对象都将得到状态改变的通知。 订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Event Channel),当发布者(Publisher)发布该事件(Publish Event)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。

创建思路

  • 创建一个 EventEmitter

  • 在该类上创建一个事件中心(Map)

  • on 方法用来把函数 fn 都加到事件中心中(订阅者注册事件到调度中心)

  • emit 方法取到 arguments 里第一个当做 event,根据 event 值去执行对应事件中心中的函数(发布者发布事件到调度中心,调度中心处理代码)

  • off 方法可以根据 event 值取消订阅(取消订阅)

  • once 方法只监听一次,调用完毕后删除缓存函数(订阅一次)

  • 注册一个 newListener 用于监听新的事件订阅

第一步,创建一个类,并初始化一个事件存储中心

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }
}
复制代码

第二步,实现事件的订阅方法 on

基本思路:将事件回调函数存储到对应的事件上

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }

    on(eventName, callback){
        // 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
        const callbacks = this._events[eventName] || [];
        callbacks.push(callback);
        this._events[eventName] = callbacks
    }
}
复制代码

第三步,实现事件的发布方法 emit

基本思路:获取到事件对应的回调函数依次执行

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }

    // args 用于收集发布事件时传递的参数
    emit(eventName, ...args){
        const callbacks = this._events[eventName] || [];
        callbacks.forEach(cb => cb(...args))
    }
}
复制代码

第四步,实现事件的取消订阅方法 off

基本思路:找到事件对应的回调函数,删除对应的回调函数

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }
    
    
    off(eventName, callback){
        const callbacks = this._events[eventName] || []

        const newCallbacks = callbacks.filter(fn => fn != callback && fn.initialCallback != callback /* 用于once的取消订阅 */)

        this._events[eventName] = newCallbacks;
    }
}
复制代码

第五步,实现事件的单次订阅方法 once

基本思路: 1.先注册 2.事件执行后取消订阅

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }
    
    // 
    once(eventName, callback){
        // 由于需要在回调函数执行后,取消订阅当前事件,所以需要对传入的回调函数做一层包装,然后绑定包装后的函数
        const one = (...args)=>{
            // 执行回调函数
            callback(...args)
            // 取消订阅当前事件
            this.off(eventName, one)
        }
        // 考虑:如果当前事件在未执行,被用户取消订阅,能否取消?



        // 由于:我们订阅事件的时候,修改了原回调函数的引用,所以,用户触发 off 的时候不能找到对应的回调函数
        // 所以,我们需要在当前函数与用户传入的回调函数做一个绑定,我们通过自定义属性来实现
        one.initialCallback = callback;
        this.on(eventName, one)
    }
}
复制代码
复制代码

第六步,注册一个 newListener 用于监听新的事件订阅

基本思路:在用户注册的事件的时候,发布一下newListener事件

class EventEmitter{
    // 用来存放注册的事件与回调
    constructor(){
        this._events = {};
    }

    on(eventName, callback){

        // 如果绑定的事件不是newListener 就触发改回调
        if(this._events[eventName]){
            if(this.eventName !== "newListener"){
                this.emit("newListener", eventName)
            }
        }
        // 由于一个事件可能注册多个回调函数,所以使用数组来存储事件队列
        const callbacks = this._events[eventName] || [];
        callbacks.push(callback);
        this._events[eventName] = callbacks
    }
}
复制代码
复制代码

测试用例


const events = new EventEmitter()

events.on("newListener", function(eventName){
    console.log(`eventName`, eventName)
})

events.on("hello", function(){
    console.log("hello");
})

let cb = function(){
    console.log('cb');
}
events.on("hello", cb)

events.off("hello", cb)

function once(){
    console.log("once");
}
events.once("hello", once)

events.off("hello", once)
events.emit("hello")
events.emit("hello")

复制代码
复制代码

完整的代码

class EventEmitter{
    constructor(){
        this._events = {};
    }

    on(eventName, callback){
        if(this._events[eventName]){
            if(this.eventName !== "newListener"){
                this.emit("newListener", eventName)
            }
        }
        const callbacks = this._events[eventName] || [];
        callbacks.push(callback);
        this._events[eventName] = callbacks
    }

    emit(eventName, ...args){
        const callbacks = this._events[eventName] || [];
        callbacks.forEach(cb => cb(...args))
    }

    once(eventName, callback){
        const one = (...args)=>{
            callback(...args)
            this.off(eventName, one)
        }
        one.initialCallback = callback;
        this.on(eventName, one)
    }

     off(eventName, callback){
        const callbacks = this._events[eventName] || []
        const newCallbacks = callbacks.filter(fn => fn != callback && fn.initialCallback != callback /* 用于once的取消订阅 */)
        this._events[eventName] = newCallbacks;
    }
}
复制代码
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享