Vue Socket.io源码解读

背景

有一个项目,今年12月份开始重构,项目涉及到了socket。但是socket用的是以前一个开发人员封装的包(这个一直被当前的成员吐槽为什么不用已经千锤百炼的轮子)。因此,趁着这个重构的机会,将vue-socket.io引入,后端就用socket.io。我也好奇看了看vue-socket.io的源码(我不会说是因为这个库的文档实在太简略了,我为了稳点去看源码了解该怎么用)

开始

文件架构

我们主要看src下的三个文件,可以看出该库是用了观察者模式

Main.js

// 这里创建一个observe对象,具体做了什么可以看Observer.js文件
let observer = new Observer(connection, store)

// 将socket挂载到了vue的原型上,然后就可以
// 在vue实例中就可以this.$socket.emit('xxx', {})
Vue.prototype.$socket = observer.Socket;
import store from './yourstore'
Vue.use(VueSocketio, socketio('http://socketserver.com:1923'), store);

我们如果要使用这个库的时候,一般是这样写的代码(上图2)。上图一的connection和store就分别是图二的后两个参数。意思分别为socket连接的url和vuex的store啦。图一就是将这两个参数传进Observer,新建了一个observe对象,然后将observe对象的socket属性挂载在Vue原型上。那么我们在Vue的实例中就可以直接 this.$sockets.emit('xxx', {})了

// 👇就是在vue实例的生命周期做一些操作
Vue.mixin({
  created(){
    let sockets = this.$options['sockets']

    this.$options.sockets = new Proxy({}, {
      set: (target, key, value) => {
        Emitter.addListener(key, value, this)
        target[key] = value
        return true;
      },
      deleteProperty: (target, key) => {
        Emitter.removeListener(key, this.$options.sockets[key], this)
        delete target.key;
        return true
      }
    })

    if(sockets){
      Object.keys(sockets).forEach((key) => {
        this.$options.sockets[key] = sockets[key];
      });
    }
  },
  /**
   * 在beforeDestroy的时候,将在created时监听好的socket事件,全部取消监听
   * delete this.$option.sockets的某个属性时,就会将取消该信号的监听
   */
  beforeDestroy(){
    let sockets = this.$options['sockets']

    if(sockets){
      Object.keys(sockets).forEach((key) => {
        delete this.$options.sockets[key]
      });
    }
  }

下面就是在Vue实例的生命周期做一些操作。创建的时候,将实例中的$options.sockets的值先缓存下来,再将$options.sockets指向一个proxy对象,这个proxy对象会拦截外界对它的赋值和删除属性操作。这里赋值的时候,键就是socket事件,值就是回调函数。赋值时,就会监听该事件,然后将回调函数,放进该socket事件对应的回调数组里。删除时,就是取消监听该事件了,将赋值时压进回调数组的那个回调函数,删除,表示,我不监听了。这样写法,其实就跟vue的响应式一个道理。也因此,我们就可以动态地添加和移除监听socket事件了,比如this.$option.sockets.xxx = () => ()和 delete this.$option.sockets.xxx。最后将缓存的值,依次赋值回去,那么如下图的写法就会监听到事件并执行回调函数了:

var vm = new Vue({
 sockets:{
  connect: function(){
   console.log('socket connected')
  },
  customEmit: function(val){
   console.log('this method was fired by the socket server. eg: io.emit("customEmit", data)')
  }
 },
 methods: {
  clickButton: function(val){
    // $socket is socket.io-client instance
    this.$socket.emit('emit_method', val);
  }
 }
})

Emitter.js

Emitter.js主要是写了一个Emitter对象,该对象提供了三个方法:

addListener

addListener(label, callback, vm) {
  // 回调函数类型是回调函数才对
  if(typeof callback == 'function'){
    // 这里就很常见的写法了,判断map中是否已经注册过该事件了
    // 如果没有,就初始化该事件映射的值为空数组,方便以后直接存入回调函数
    // 反之,直接将回调函数放入数组即可
    this.listeners.has(label) || this.listeners.set(label, []);
    this.listeners.get(label).push({callback: callback, vm: vm});

    return true
  }

  return false
}

其实很常规啦,实现发布订阅者模式或者观察者模式代码的同学都很清楚这段代码的意思。Emiiter用一个map来存储事件以及它对应的回调事件数组。这段代码先判断map中是否之前已经存储过了该事件,如果没有,初始化该事件对应的值为空数组,然后将当前的回调函数,压进去,反之,直接压进去。

removeListener

if (listeners && listeners.length) {
  index = listeners.reduce((i, listener, index) => {
    return (typeof listener.callback == 'function' && listener.callback === callback && listener.vm == vm) ?
      i = index :
      i;
  }, -1);

  if (index > -1) {
    listeners.splice(index, 1);
    this.listeners.set(label, listeners);
    return true;
  }
}
return false;

这里也很简单啦,获取该事件对应的回调数组。如果不为空,就去寻找需要移除的回调,找到后,直接删除,然后将新的回调数组覆盖原来的那个就可以了

emit

if (listeners && listeners.length) {
  listeners.forEach((listener) => {
    listener.callback.call(listener.vm,...args)
  });
  return true;
}
return false;

这里就是监听到事件后,执行该事件对应的回调函数,注意这里的call,因为监听到事件后我们可能要修改下vue实例的数据或者调用一些方法,用过vue的同学都知道我们都是this.xxx来调用的,所以一定得将回调函数的this指向vue实例,这也是为什么存回调事件时也要把vue实例存下来的原因。

Observer.js

constructor(connection, store) {
  // 这里很明白吧,就是判断这个connection是什么类型
  // 这里的处理就是你可以传入一个连接好的socket实例,也可以是一个url
  if(typeof connection == 'string'){
    this.Socket = Socket(connection);
  }else{
    this.Socket = connection
  }

  // 如果有传进vuex的store可以响应在store中写的mutations和actions
  // 这里只是挂载在这个oberver实例上
  if(store) this.store = store;

  // 监听,启动!
  this.onEvent()

}

这个Observer.js里也主要是写了一个Observer的class,以上是它的构造函数,构造函数第一件事是判断connection是不是字符串,如果是就构建一个socket实例,如果不是,就大概是个socket的实例了,然后直接挂载在它的对象实例上。其实这里我觉得可以参数检查严格点, 比如字符串被人搞怪地可能会传入一个非法的url,对吧。这个时候判断下,抛出一个error提醒下也好,不过应该也没人这么无聊吧,2333。然后如果传入了store,也挂在对象实例上吧。最后就启动监听事件啦。我们看看onEvent的逻辑

  onEvent(){
    // 监听服务端发来的事件,packet.data是一个数组
    // 第一项是事件,第二个是服务端传来的数据
    // 然后用emit通知订阅了该信号的回调函数执行
    // 如果有传入了vuex的store,将该事件和数据传入passToStore,执行passToStore的逻辑
    var super_onevent = this.Socket.onevent;
    this.Socket.onevent = (packet) => {
      super_onevent.call(this.Socket, packet);

      Emitter.emit(packet.data[0], packet.data[1]);

      if(this.store) this.passToStore('SOCKET_'+packet.data[0], [ ...packet.data.slice(1)])
    };

    // 这里跟上面意思应该是一样的,我很好奇为什么要分开写,难道上面的写法不会监听到下面的信号?
    // 然后这里用一个变量暂存this
    // 但是下面都是箭头函数了,我觉得没必要,毕竟箭头函数会自动绑定父级上下文的this
    let _this = this;

    ["connect", "error", "disconnect", "reconnect", "reconnect_attempt", "reconnecting", "reconnect_error", "reconnect_failed", "connect_error", "connect_timeout", "connecting", "ping", "pong"]
      .forEach((value) => {
        _this.Socket.on(value, (data) => {
          Emitter.emit(value, data);
          if(_this.store) _this.passToStore('SOCKET_'+value, data)
        })
      })
  }

这里就是有点类似重载onevent这个函数了,监听到事件后,将数据拆包,然后通知执行回调和传递给store。大体的逻辑是这样子。然后这代码实现有两部分,第一部分和第二部分逻辑基本一样。只是分开写。(其实我也不是很懂啦,如果很有必要的话,我猜第一部分的写法还监听不了第二部分的事件吧,所以要另外监听)。最后只剩下一个passToStore了,其实也很容易懂

 passToStore(event, payload){
   // 如果事件不是以SOCKET_开头的就不用管了
   if(!event.startsWith('SOCKET_')) return

   // 这里遍历vuex的store中的mutations
   for(let namespaced in this.store._mutations) {
     // 下面的操作是因为,如果store中有module是开了namespaced的,会在mutation的名字前加上 xxx/
     // 这里将mutation的名字拿出来
     let mutation = namespaced.split('/').pop()
     // 如果名字和事件是全等的,那就发起一个commit去执行这个mutation
     // 也因此,mutation的名字一定得是 SOCKET_开头的了
     if(mutation === event.toUpperCase()) this.store.commit(namespaced, payload)
   }
   // 这里类似上面
   for(let namespaced in this.store._actions) {
     let action = namespaced.split('/').pop()

     // 这里强制要求了action的名字要以 socket_ 开头
     if(!action.startsWith('socket_')) continue

     // 这里就是将事件转成驼峰式
     let camelcased = 'socket_'+event
         .replace('SOCKET_', '')
         .replace(/^([A-Z])|[\W\s_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())

     // 如果action和事件全等,那就发起这个action
     if(action === camelcased) this.store.dispatch(namespaced, payload)
   }
 }

passToStore嘛其实就是做两个事情,一个是获取与该事件对应的mutation,然后发起一个commit,一个是获取与该事件对应的action,然后dispatch。只是这里的实现对mutations和actions的命名有了要求,比如mutations的命名一定得是SOCKET_开头,action就是一个得socket_开头,然后还得是驼峰式命名。

最后

首先,这个源码是不是略有点简单,哈哈哈,不过,能给你们一些帮助,我觉得也挺好的

然后,就是如果上面我说的有是很对的,请大家去这里发issue或者直接评论吧

最后,源码的详细的注释在这里

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

您可能感兴趣的文章:

  • vue+socket.io+express+mongodb 实现简易多房间在线群聊示例
  • vue + socket.io实现一个简易聊天室示例代码
(0)

相关推荐

  • vue+socket.io+express+mongodb 实现简易多房间在线群聊示例

    项目简介 主要是通过做一个多人在线多房间群聊的小项目.来练手全栈技术的结合运用. 项目源码:chat-vue-node 主要技术: vue2全家桶 + socket.io + node(express) + mongodb(mongoose) 环境配置: 需安装配置好 node,mongodb环境(参考:http://www.jb51.net/article/109449.htm); 建议安装 Robomogo 客户端来管理mongodb数据. 编译运行: 1.开启MongoDB服务,新建命令行

  • vue + socket.io实现一个简易聊天室示例代码

    vue + vuex + elementUi + socket.io实现一个简易的在线聊天室,提高自己在对vue系列在项目中应用的深度.因为学会一个库或者框架容易,但要结合项目使用一个库或框架就不是那么容易了.功能虽然不多,但还是有收获.设计和实现思路较为拙劣,恳请各位道友指正. 可以达到的需求 能查看在线用户列表 能发送和接受消息 使用到的框架和库 socket.io做为实时通讯基础 vuex/vue:客户端Ui层使用 Element-ui:客户端Ui组件 类文件关系图 服务端: 客户端: 服

  • Vue Socket.io源码解读

    背景 有一个项目,今年12月份开始重构,项目涉及到了socket.但是socket用的是以前一个开发人员封装的包(这个一直被当前的成员吐槽为什么不用已经千锤百炼的轮子).因此,趁着这个重构的机会,将vue-socket.io引入,后端就用socket.io.我也好奇看了看vue-socket.io的源码(我不会说是因为这个库的文档实在太简略了,我为了稳点去看源码了解该怎么用) 开始 文件架构 我们主要看src下的三个文件,可以看出该库是用了观察者模式 Main.js // 这里创建一个obser

  • vue源码解读子节点优化更新

    目录 前言 优化前存在的问题 优化策略分析 源码解析 小结 前言 Vue中更新节点,当新 VNode 和旧 VNode 都是元素节点且都有子节点时,Vue会循环对比新旧 VNode 的子节点数组,然后根据不同情况做不同处理. 虽然这种方法能解决问题,但是当更新子节点特别多时,循环算法的时间复杂度就会很高,所以Vue对此进行了优化. 优化前存在的问题 现在有新的 newChildren 数组和旧的 oldChildren 数组: newChildren = ['a','b','c','d']; o

  • Vue 2源码解读$mount函数原理

    目录 1. $mount 函数来源 2. runtime 运行时的 $mount 函数 2.1 mountComponent 函数 2.2 _update 函数(首次渲染) 3. runtime-with-compiler 的 $mount 函数 4. runtime 对 Vue 构造函数的其他修改 1. $mount 函数来源 上一节虽然直接从 core 目录下找到了 Vue 的构造函数定义,但是缺少 $mount 方法.所以直接从开发过程中使用的 vue.esm.js 找到对应的源码入口.

  • vue3 源码解读之 time slicing的使用方法

    今天给大家带来一篇源码解析的文章,emm 是关于 vue3 的,vue3 源码放出后,已经有很多文章来分析它的源码,我觉得很快又要烂大街了,哈哈 不过今天我要解析的部分是已经被废除的 time slicing 部分,这部分源码曾经出现在 vue conf 2018 的视频中,但是源码已经被移除掉了,之后可能也不会有人关注,所以应该不会烂大街 打包 阅读源码之前,需要先进行打包,打包出一份干净可调试的文件很重要 vue3 使用的 rollup 进行打包,我们需要先对它进行改造 import cle

  • 详解webpack-dev-middleware 源码解读

    前言 Webpack 的使用目前已经是前端开发工程师必备技能之一.若是想在本地环境启动一个开发服务,大家只需在 Webpack 的配置中,增加 devServer的配置来启动.devServer 配置的本质是 webpack-dev-server 这个包提供的功能,而 webpack-dev-middleware 则是这个包的底层依赖. 截至本文发表前,webpack-dev-middleware 的最新版本为 webpack-dev-middleware@3.7.2,本文的源码来自于此版本.本

  • Python bsonrpc源码解读

    bsonrpc 是python中⼀个基于json或bson的远程过程调⽤的库,提供了服务端与客户端实现,其底层采⽤的是基于TCP连接的通信. 程序结构 bsonrpc主要包括以下⽂件: concurrent.py:针对两种并发⽅式(threading线程对象.gevent协程对象)涉及的相应组件(Queue,Event,Lock等)提供统⼀的对外的⽣成接⼝:spawn(),new_promise(),new_queue(), new_lock()等: definitions.py:定义rpc的消

  • 详解go中panic源码解读

    panic源码解读 前言 本文是在go version go1.13.15 darwin/amd64上进行的 panic的作用 panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine中递归执行调用方的defer: recover可以中止panic造成的程序崩溃.它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用: 举个栗子 package main import "fmt" func main() { fmt.

  • Java 集合框架掌握 Map 和 Set 的使用(内含哈希表源码解读及面试常考题)

    目录 1. 搜索 1.1 场景引入 1.2 模型 2. Map 2.1 关于 Map 的介绍 2.2 关于 Map.Entry<K, V> 的介绍 2.3 Map 的常用方法说明 2.4 关于 HashMap 的介绍 2.5 关于 TreeMap 的介绍 2.6 HashMap 和 TreeMap 的区别 2.7 Map 使用示例代码 3. Set 3.1 关于 Set 的介绍 3.1 Set 的常用方法说明 3.3 关于 TreeSet 的介绍 3.4 关于 HashSet 的介绍 3.5

  • 深入了解Java线程池:从设计思想到源码解读

    目录 为什么需要线程池 线程池设计思路 线程池的工作机制 线程池的参数及使用 线程池的状态 提交任务 任务队列 线程工厂 拒绝策略 关闭线程池 Executors 静态工厂 合理地配置线程池 线程池的监控 源码分析 execute addWorker Worker runWorker getTask processWorkerExit 面试题 为什么需要线程池 我们知道创建线程的常用方式就是 new Thread() ,而每一次 new Thread() 都会重新创建一个线程,而线程的创建和销毁

  • SpringBoot自定义加载yml实现方式,附源码解读

    目录 自定义加载yml,附源码解读 解决方法 源码解读 如何引入多个yml方法 方案一:无前缀,使用@Value注解 方案二:有前缀,无需@Value注解 自定义加载yml,附源码解读 昨天在对公司的微服务配置文件标准化的过程中,发现将原来的properties文件转为yml文件之后,微服务module中标记有@Configuration的配置类都不能正常工作了,究其原因,是由于@PropertySource属性默认只用于标记并告诉spring boot加载properties类型的文件 spr

随机推荐