字节面试官:“手写个 eventBus 吧”

3,891 阅读3分钟

本文正在参加「金石计划」

面试过程

笔者近期参加了字节飞书文档部门的面试,总的来说不难,但是挂的莫名其妙,可能是最近根本就没hc?

听内推人说我一面那会我的岗位他已经搜不到了(

在面试过程中字节面试官先问了我 Vue 兄弟组件通信的方式,我不假思索的答了一堆,然后突然想起来还有个 eventBus,然后就答上去了。

面试官接着问,你知道如何实现一个 eventBus 吗?

因为实习写了一段时间 React,Vue2细节不太记得了,我当时听到这问题心里咯噔一下。

什麽???写一下 eventBus ?

我当时思考了半分钟吧,我说这玩意应该和发布订阅模式挺像的,我就试探了一下。

“这个应该是用发布订阅实现的,我来实现一下吧”

面试官就看着我,没说话(

image.png

我就开始写了

class EventBus {
    constructor() {
        this.queue = []
    }
    
    on(name, val) {
        this.queue.push([name, val])
    }
    
    emit(name) {
        const idx = this.queue.findIndex(item => item[0] === name)
        return this.queue[idx][1]
    }
}

const eventBus = new EventBus()
eventBus.on('test', 12)
eventBus.emit('test')

发布订阅当时实现也没复习,写的就是这个鬼样子,超级简短。

面试官:“如果我两次赋值用同一个name呢?”

比如

eventBus.on('test', 12)
eventBus.on('test', 13)

我当时脑袋一抽:“应该要覆盖掉吧”

然后写了一个版给他看

class EventBus {
    constructor() {
        this.queue = []
        this.names = []
    }
    
    on(name, val) {
        if(this.names.indexOf(name) === -1) {
            this.names.push(name)
            this.queue.push([name, val])
        } else {
            const idx = this.queue.findIndex(item => item[0] === name)
            this.queue[idx][1] = val
        }
    }
    
    emit(name) {
        const idx = this.queue.findIndex(item => item[0] === name)
        return this.queue[idx][1]
    }
}

const eventBus = new EventBus()
eventBus.on('test', 12)
eventBus.on('test', 13)
eventBus.emit('test')

后来仔细看发布订阅这么个模式的时候,发现这里是多个回调函数顺次执行。

他没说啥,就让我写下一个手写题了。

正解

  1. 首先得用一个对象来存起来
  2. 多个同名值用一个 Array 来存

所以当时面试官应该是要考我这一点,但没答出来。

  1. 其他都在注释里了,详情可以看看修言的设计模式小测,上班摸鱼的时候刷的,非常顶,刷着刷着就看到之前的面试题了。
class EventEmitter {
  constructor() {
    // handlers是一个map,用于存储事件与回调之间的对应关系
    this.handlers = {}
  }

  // on方法用于安装事件监听器,它接受目标事件名和回调函数作为参数
  on(eventName, cb) {
    // 先检查一下目标事件名有没有对应的监听函数队列
    if (!this.handlers[eventName]) {
      // 如果没有,那么首先初始化一个监听函数队列
      this.handlers[eventName] = []
    }

    // 把回调函数推入目标事件的监听函数队列里去
    this.handlers[eventName].push(cb)
  }

  // emit方法用于触发目标事件,它接受事件名和监听函数入参作为参数
  emit(eventName, ...args) {
    // 检查目标事件是否有监听函数队列
    if (this.handlers[eventName]) {
      // 这里需要对 this.handlers[eventName] 做一次浅拷贝,主要目的是为了避免通过 once 安装的监听器在移除的过程中出现顺序问题
      const handlers = this.handlers[eventName].slice()
      // 如果有,则逐个调用队列里的回调函数
      handlers.forEach((callback) => {
        callback(...args)
      })
    }
  }

  // 移除某个事件回调队列里的指定回调函数
  off(eventName, cb) {
    const callbacks = this.handlers[eventName]
    const index = callbacks.indexOf(cb)
    if (index !== -1) {
      callbacks.splice(index, 1)
    }
  }

  // 为事件注册单次监听器
  once(eventName, cb) {
    // 对回调函数进行包装,使其执行完毕自动被移除
    const wrapper = (...args) => {
      cb(...args)
      this.off(eventName, wrapper)
    }
    this.on(eventName, wrapper)
  }
}

看完点个赞再走吧~