liam61 / blog

Lawler's Blog 不定期分享一些前端技术
156 stars 28 forks source link

【面经】字节跳动 笔试1:实现一个类可以完成事件 on,once,trigger,off #1

Open liam61 opened 5 years ago

liam61 commented 5 years ago

手撸一个事件机制

关键词:发布-订阅模式

其实核心就是维护一个对象,对象的 key 存的是事件 type,对应的 value 为触发相应 type 的回调函数,即 listeners,然后 trigger 时遍历通知,即 forEach 进行回调执行。

class EventTarget {
  constructor() {
    this.listeners = {}; // 储存事件的对象
  }

  on(type, callback) {
    if (!this.listeners[type]) this.listeners[type] = []; // 如果是第一次监听该事件,则初始化数组
      this.listeners[type].push(callback);
  }

  once(type, callback) {
   if (!this.listeners[type]) this.listeners[type] = [];
      callback._once = true; // once 只触发一次,触发后 off 即可
      this.listeners[type].push(callback);
  }

  off(type, callback) {
    const listeners = this.listeners[type];
    if (Array.isArray(listeners)) {
      // filter 返回新的数组,会每次对 this.listeners[type] 分配新的空间
      // this.listeners[type] = listeners.filter(l => l !== callback);
      const index = listeners.indexOf(callback); // 根据 type 取消对应的回调
      this.listeners[type].splice(index, 1); // 用 splice 要好些,直接操作原数组

      if (this.listeners[type].length === 0) delete this.listeners[type]; // 如果回调为空,删除对该事件的监听
    }
  }

  trigger(event) {
    const { type } = event; // type 为必传属性
    if (!type) throw new Error('没有要触发的事件!');

    const listeners = this.listeners[type]; // 判断是否之前对该事件进行监听了
    if (!listeners) throw new Error(`没有对象监听 ${type} 事件!`);

    if (!event.target) event.target = this;

    listeners.forEach(l => {
      l(event);
      if (l._once) this.off(type, l); // 如果通过 once 监听,执行一次后取消
    });
  }
}

// 测试
function handleMessage(event) { console.log(`message received: ${ event.message }`); }

function handleMessage2(event) { console.log(`message2 received: ${ event.message }`); }

const target = new EventTarget();

target.on('message', handleMessage);
target.on('message', handleMessage2);
target.trigger({ type: 'message', message: 'hello custom event' }); // 打印 message,message2

target.off('message', handleMessage);
target.trigger({ type: 'message', message: 'off the event' }); // 只打印 message2

target.once('words', handleMessage);
target.trigger({ type: 'words', message: 'hello2 once event' }); // 打印 words
target.trigger({ type: 'words', message: 'hello2 once event' }); // 报错:没有对象监听 words 事件!
linfanxxxx commented 5 years ago

大佬,如果on的callback是个匿名函数,就无法off了咋办

liam61 commented 5 years ago

@linfanxxxx 这个模拟的是 dom2 的方法,一般都是用带名的,你可以回想 button 如何定义事件的 div.addEventListener('click', fn); div.removeEventListener('click', fn); 当然你也可以用 dom0 级事件,div.onclick = null; 这样回收机制机就会清理内存了

AqingCyan commented 4 years ago

大佬,我想问一下你是社招还是校招啊。还有就是这是观察者模式还是发布订阅啊,在我印象里面,发布者和订阅者在互相能知晓的情况下,应该是观察者啊。

liam61 commented 4 years ago

@AqingCyan 校招。观察者和订阅发布模式是有区别的,主要是看有无中介者来传递消息,也就是像你说的是否能相互感知,但平时的话也可以不做区分,这里手撸的事件的话算是观察者模式