shfshanyue / Daily-Question

互联网大厂内推及大厂面经整理,并且每天一道面试题推送。每天五分钟,半年大厂中
https://q.shanyue.tech
4.94k stars 511 forks source link

【Q613】如何使用 JS 实现一个发布订阅模式 #631

Open shfshanyue opened 3 years ago

shfshanyue commented 3 years ago

使用 JS 实现一个发布订阅器,Event,示例如下:

const e = new Event()

e.on('click', x => console.log(x.id))

e.once('click', x => console.log(id))

//=> 3
e.emit('click', { id: 3 })

//=> 4
e.emit('click', { id: 4 })

API 如下:

class Event {
  emit (type, ...args) {
  }

  on (type, listener) {
  }

  once (type, listener) {
  }

  off (type, listener) {
  }
}
shfshanyue commented 3 years ago

一个简单的订阅发布模式实现如下,主要有两个核心 API

实现该模式,使用一个 events 维护发布的事件:

const events = {
  click: [{
    once: true,
    listener: callback,
  }, {
    listener: callback
  }]
}

具体实现代码如下所示

class Event {
  events = {}

  emit (type, ...args) {
    const listeners = this.events[type]
    for (const listener of listeners) {
      listener.listener(...args)
      if (listener.once) {
        this.off(type, listener.listener)
      }
    }
  }

  on (type, listener) {
    this.events[type] = this.events[type] || []
    this.events[type].push({ listener })
  }

  once (type, listener) {
    this.events[type] = this.events[type] || []
    this.events[type].push({ listener, once: true })
  }

  off (type, listener) {
    this.events[type] = this.events[type] || []
    this.events[type] = this.events[type].filter(listener => listener.listener !== listener)
  }
}

以上代码不够优雅,且有点小瑕疵,再次实现如下,代码可见 如何实现发布订阅器 - codepen

class Event {
  events = {}

  emit (type, ...args) {
    const listeners = this.events[type]
    for (const listener of listeners) {
      listener(...args)
    }
  }

  on (type, listener) {
    this.events[type] = this.events[type] || []
    this.events[type].push(listener)
  }

  once (type, listener) {
    const callback = (...args) => {
      this.off(type, callback)
      listener(...args)
    }
    this.on(type, callback)
  }

  off (type, listener) {
    this.events[type] = this.events[type] || []
    this.events[type] = this.events[type].filter(callback => callback !== listener)
  }
}

const e = new Event()

const callback = x => { console.log('Click', x.id) }
e.on('click', callback)
e.on('click', callback)

// 只打印一次
const onceCallback = x => console.log('Once Click', x.id)
e.once('click', onceCallback)
e.once('click', onceCallback)

//=> 3
e.emit('click', { id: 3 })

//=> 4
e.emit('click', { id: 4 })
heretic-G commented 3 years ago

class Center {
    eventMap = {}
    on (event, fun) {
        this.#add(event, fun, 'on')
    }

    once (event, fun) {
        this.#add(event, fun, 'once')
    }

    #add (event, fun, type) {
        if (typeof fun !== 'function') throw new TypeError(`${fun} is not a function`)
        if (!event) throw new Error(`need type`)
        if (!this.eventMap[event]) {
            this.eventMap[event] = []
        }
        this.eventMap[event].push({
            event: fun,
            type: type
        })
    }

    emit (event, ...args) {
        if (this.eventMap[event]) {
            this.eventMap[event] = this.eventMap[event].filter(curr => {
                curr.data(...args)
                return curr.type !== 'once'
            })
        }
    }

    remove (event, fun) {
        if (this.eventMap[event]) {
            this.eventMap[event] = this.eventMap[event].filter(curr => {
                return curr.event !== fun
            })
        }
    }
}
infjer commented 3 years ago

之前去B站面试,还追问了循环订阅如何处理。

shfshanyue commented 3 years ago

@infjer 何为循环订阅

leblancy commented 3 years ago
class Emitter {
  constructor() {
    this.events = {};
  }

  emit(type, ...args) {
    if (this.events[type] && this.events[type].length > 0) {
      this.events[type].forEach(cb => {
        cb.apply(this, args);
      });
    }
  }

  on(type, cb) {
    if (!this.events[type]) {
      this.events[type] = [];
    }
    this.events[type].push(cb);
  }

  once(type, cb) {
    const func = (...args) => {
      this.off(type, func);
      cb.apply(this, args);
    };

    this.on(type, func);
  }

  off(type, cb) {
    if (!cb) {
      this.events[type] = null;
    } else {
      this.events[type].filter(exec => exec !== cb);
    }
  }
}
Hazel-Lin commented 1 year ago
// 实现发布订阅模式
// on 订阅
// emit 发布
// off 取消订阅

class EventEmitter{
  constructor(){
    this.events = {}
  }
  // 订阅
  on(type, callback){
    this.events[type] = this.events[type] || [];
    this.events[type].push(callback);
  }
  // 取消订阅
  off(type, callback){
    if(!this.events[type]) return;
    this.events[type] = this.events[type].filter(event => event !== callback)
  }
  // 发布
  emit(type, ...args){
   if(!this.events[type]) return
    // 遍历事件
    this.events[type].forEach(event => {
      event(...args)
    })
  }
}

const e = new EventEmitter()
const callback2 = (data) => console.log(`${data.name}`);

e.on('click',callback2)

e.emit('click',{name:'xiaoming'})

e.off('click', callback2)

e.emit('click',{name:'lili'})