面试官:讲一下Vue的发布订阅模式?

大家好,我是前端小张同学,很久没更新文章了,祝大家2024年万事如意,最近有时间看一些非业务层面的代码,面试官也经常会问道,那今天就跟大家分享一下 Vue的发布订阅模式 以及它的源码实现吧。

1:什么是发布订阅模式?

示例: 微信中的公众号消息推送,我们每个用户关注公众号以后公众号会不定时的给我们推送一些文章,其实在用户和微信公众号之间就已经悄然地形成了一种发布订阅模式。

2: 发布订阅的核心介绍

  1. $on

    功能 :负责事件的监听注册,将事以及对应的回调收集到 事件中心,在调用 $emit 的时候 去触发对应的回调。

  2. $off

    功能:负责事件的销毁和取消监听,将事件中心的callback 统一清除 or 指定的 callback 进行清除

  3. $emit

    功能:负责事件的发射与触发,可以将指定参数传递给回调函数,进行发布通知。

  4. $once(vue内部拓展)

    功能:负责监听一个自定义事件,但是只触发一次。一旦触发之后,监听器就会被移除。

3: Vue内部源码实现,它干了 什么事情?

源码文件: vue\src\core\instance\events.ts

$on 方法,做了什么事情?

1:在Vue 实例 原型身上挂在 $on方法

2:如果 event 是一个数组 则需要循环注册该事件到 事件中心(vm._events

3:循环注册该事件,然后收集到事件中心

4:判断事件中心是否有该事件映射,如果有则将 传入的 callback push 映射的数组中,如果没有 则 开辟一个数组空间 push

5:如果注册时事件 是 hook 开头的 则 更新 hash标记

js 复制代码
  const hookRE = /^hook:/
  Vue.prototype.$on = function (  //在Vue 实例 原型身上挂在 $on方法
    event: string | Array<string>, // 传入 事件名称
    fn: Function // 传入事件的回调函数
  ): Component {
    const vm: Component = this
    if (isArray(event)) { // 如果 event 是一个数组 则需要循环注册该事件到 事件中心
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$on(event[i], fn) // 循环注册
      }
    } else {
      (vm._events[event] || (vm._events[event] = [])).push(fn) // 如果 事件中心 _events 取的到 该事件 则 将 callback push 进去 否则 新建一个数组 往里面 push
      // optimize hook:event cost by using a boolean flag marked at registration
      // instead of a hash lookup
      if (hookRE.test(event)) { // 如果注册时事件 是 hook 开头的 则 更新 hash标记
        vm._hasHookEvent = true
      }
    }
    return vm  // 返回实例
  }

$off 方法,做了什么事情?

1: 如果 $off 未提供参数 直接调用 则 直接移除注册中心的所有事件,相当于 清空操作。

2: 如果事件是一个数组 则 循环 移出事件

3: 在vm._events 身上根据 name 取出 callbacks 数组 ,如果没有 则返回 this实例

4: 你在销毁事件,但callback 不存在,那默认将你映射的数组集合 全部清空,这一步也就对应着Vue官方$off用法,有兴趣可以去看看

5: 最终取映射的回调数组 移除 所有的 callback

js 复制代码
  // 销毁一些 注册在控制中心的事件
  Vue.prototype.$off = function (
    event?: string | Array<string>, // 传入 事件名称 or 事件名称数组
    fn?: Function // 移除的函数
  ): Component {
    const vm: Component = this
    // all
    if (!arguments.length) { // 如果 $off 未提供参数 直接调用 则 直接移除注册中心的所有事件
      vm._events = Object.create(null)
      return vm
    }
    // array of events
    if (isArray(event)) { // 如果事件是一个数组 则 循环 移出事件
      for (let i = 0, l = event.length; i < l; i++) {
        vm.$off(event[i], fn)
      }
      return vm
    }
    // specific event
    const cbs = vm._events[event!]
    if (!cbs) { // 如果 在事件注册中心取不到 cbs 则 返回this 实例
      return vm
    }
    if (!fn) { // $off 的 callback 都不存在的话 直接移除 事件中心的 callbacks 数组
      vm._events[event!] = null
      return vm
    }
    // specific handler
    let cb // 移除 所有的 callback
    let i = cbs.length
    while (i--) {
      cb = cbs[i]
      if (cb === fn || cb.fn === fn) {
        cbs.splice(i, 1)
        break
      }
    }
    return vm
  }

$emit 方法,做了什么事情?

1: 获得事件中下的映射 回调数组

2: 将取出的数组 , 转换为真实数组,这一步可能是为了边界处理,怕出现类数组的情况

3: 将 arguments 数组转换为 真实数组,作为参数 传递给 invokeWithErrorHandling

4: 循环调用里面的每一个 回调函数,也就是去通知 正在监听的事件,执行回调

js 复制代码
  // 负责 触发 事件中心的
  Vue.prototype.$emit = function (event: string): Component {
    const vm: Component = this
    if (__DEV__) { // 开发环境代码 可以忽略 
      const lowerCaseEvent = event.toLowerCase()
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          `Event "${lowerCaseEvent}" is emitted in component ` +
            `${formatComponentName(
              vm
            )} but the handler is registered for "${event}". ` +
            `Note that HTML attributes are case-insensitive and you cannot use ` +
            `v-on to listen to camelCase events when using in-DOM templates. ` +
            `You should probably use "${hyphenate(
              event
            )}" instead of "${event}".`
        )
      }
    }
    let cbs = vm._events[event] // 获得所有的 事件数组
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs // 转换csb 为真实 数组
      const args = toArray(arguments, 1)
      const info = `event handler for "${event}"`
      for (let i = 0, l = cbs.length; i < l; i++) {
        invokeWithErrorHandling(cbs[i], vm, args, vm, info) // invokeWithErrorHandling 调用里面的每一个 callback 判断每一个 fn 的类型 然后调用
      }
    }
    return vm
  }

$once 方法,做了什么事情?

1: 编写了 内部的 on 方法, 该函数的核心作用 , 执行一次 callback 并且立即销毁

2: 保存 fn 到 on 身上

3: 手动的内部订阅事件,回调 即为 on 函数,这样能保证 只执行一次,先将 监听的事件 $off 掉,然后手动调用一次 fn 也就是外部传入的 callback

js 复制代码
  Vue.prototype.$once = function (event: string, fn: Function): Component {
    const vm: Component = this
    function on() {
      vm.$off(event, on) // 移出事件
      fn.apply(vm, arguments) // 调用函数
    }
    on.fn = fn // 保存 当前fn 在 on函数身上
    vm.$on(event, on) // 执行一次调用
    return vm // 返回 实例
  }

总结

其实 从Vue 的源码角度上来看发布订阅模式,其实也没有什么很难的东西,从头到尾捋一遍其实你就可以发现 Vue 内部将 一些注册的事件名称 和 callback 存入到一个 名叫 _events 对象身上,其 对应的值为 map 也就是 key Value的形式 , 而通过 $emit 去派发通知 callback 的执行,通过 $on 取订阅注册事件,以及要触发的回调,但使用过程中,$on 永远实在 mounted 的时候 才能 调用 ,相信大家 看到 vm的时候 已经明白了,this实例的用途,那 $off 对 事件中就是起到了 销毁和记忆清除的作用了,因为 既然不需要使用了那就得清除掉,就算是及时的释放资源,更主要的原因是避免重复调用,毕竟它是全局的。

结束

好了,以上就是源码的描述,到这里此篇文章就结束了,那有空我会手写一个 发布订阅模式,带大家一起实现,下方有前端的技术交流群,希望大家可以积极参与进来,一起学习,一起进步,好的,我是前端小张同学 期待你的关注。

相关推荐
灰海7 分钟前
原型与原型链到底是什么?
开发语言·前端·javascript·es6·原型模式·原生js
山河木马23 分钟前
前端学C++可太简单了:双冒号 :: 操作符
前端·javascript·c++
汪子熙24 分钟前
什么是 ArkTS
后端·面试
3Katrina24 分钟前
前端面试之防抖节流(二)
前端·javascript·面试
前端进阶者30 分钟前
天地图编辑支持删除编辑点
前端·javascript
Z字小熊饼干爱吃保安40 分钟前
面试技术问题总结一
数据库·面试·职场和发展
前端小巷子1 小时前
深入理解TCP协议
前端·javascript·面试
万少1 小时前
鸿蒙外包的十大生存法则
前端·后端·面试
顽疲1 小时前
从零用java实现 小红书 springboot vue uniapp(13)模仿抖音视频切换
java·vue.js·spring boot
半桔2 小时前
【Linux手册】从接口到管理:Linux文件系统的核心操作指南
android·java·linux·开发语言·面试·系统架构