Vue Socket.io源码解读


Posted in Javascript onFebruary 07, 2018

背景

有一个项目,今年12月份开始重构,项目涉及到了socket。但是socket用的是以前一个开发人员封装的包(这个一直被当前的成员吐槽为什么不用已经千锤百炼的轮子)。因此,趁着这个重构的机会,将vue-socket.io引入,后端就用socket.io。我也好奇看了看vue-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或者直接评论吧

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

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

Javascript 相关文章推荐
JS动态增加删除UL节点LI及相关内容示例
May 21 Javascript
js实现最短的XML格式化工具实例
Mar 12 Javascript
jquery实现手机号码选号的方法
Jul 31 Javascript
jquery实现隐藏在左侧的弹性弹出菜单效果
Sep 18 Javascript
Html5 js实现手风琴效果
Apr 17 Javascript
JS表单数据验证的正则表达式(常用)
Feb 18 Javascript
详解使用angular-cli发布i18n多国语言Angular应用
May 20 Javascript
Vue学习笔记进阶篇之vue-router安装及使用方法
Jul 19 Javascript
JS中使用textPath实现线条上的文字
Dec 25 Javascript
浅谈Webpack 是如何加载模块的
May 24 Javascript
js中事件对象和事件委托的介绍
Jan 21 Javascript
js中addEventListener()与removeEventListener()用法案例分析
Mar 02 Javascript
原生JavaScript实现的简单放大镜效果示例
Feb 07 #Javascript
Express下采用bcryptjs进行密码加密的方法
Feb 07 #Javascript
Vue Element使用icon图标教程详解(第三方)
Feb 07 #Javascript
Vue.set()实现数据动态响应的方法
Feb 07 #Javascript
vue中如何动态绑定图片,vue中通过data返回图片路径的方法
Feb 07 #Javascript
vue 动态改变静态图片以及请求网络图片的实现方法
Feb 07 #Javascript
vue进行图片的预加载watch用法实例讲解
Feb 07 #Javascript
You might like
深入eAccelerator与memcached的区别详解
2013/06/06 PHP
php网站地图生成类示例
2014/01/13 PHP
php单例模式的简单实现方法
2016/06/10 PHP
php封装的验证码类分享
2017/02/26 PHP
PHP实现字符串大小写转函数的功能实例
2019/02/06 PHP
通过正则格式化url查询字符串实现代码
2012/12/28 Javascript
jquery插件之定时查询待处理任务数量
2014/05/01 Javascript
js实现YouKu的漂亮搜索框效果
2015/08/19 Javascript
js 右侧浮动层效果实现代码(跟随滚动)
2015/11/22 Javascript
JS字符串的切分用法实例
2016/02/22 Javascript
JavaScript 函数的执行过程
2016/05/09 Javascript
JavaScript表单焦点自动切换代码
2016/07/24 Javascript
jQuery实现两列等高并自适应高度
2016/12/22 Javascript
jQuery树插件zTree使用方法详解
2017/05/02 jQuery
VUE2.0+Element-UI+Echarts封装的组件实例
2018/03/02 Javascript
layui.tree组件的使用以及搜索节点功能的实现
2019/09/26 Javascript
在Django的视图中使用数据库查询的方法
2015/07/16 Python
基于Django的python验证码(实例讲解)
2017/10/23 Python
Python continue继续循环用法总结
2018/06/10 Python
Python数据可视化库seaborn的使用总结
2019/01/15 Python
python3.6中@property装饰器的使用方法示例
2019/08/17 Python
PyTorch中的padding(边缘填充)操作方式
2020/01/03 Python
Python 如何在字符串中插入变量
2020/08/01 Python
HTML5 weui使用笔记
2019/11/21 HTML / CSS
Anya Hindmarch官网:奢侈设计师手袋及配饰
2018/11/15 全球购物
Linux如何为某个操作添加别名
2013/03/01 面试题
简述安装Slackware Linux系统的过程
2012/05/08 面试题
Linux上比较文件的命令都有哪些
2012/02/24 面试题
决心书范文
2014/03/11 职场文书
大学学习计划书范文
2014/05/02 职场文书
2014年教师节演讲稿
2014/09/03 职场文书
2014年妇委会工作总结
2014/12/10 职场文书
六五普法心得体会2016
2016/01/21 职场文书
如何写好开幕词?
2019/06/24 职场文书
pytorch 运行一段时间后出现GPU OOM的问题
2021/06/02 Python
B站评分公认最好看的动漫,你的名字评分9.9,第六备受喜欢
2022/03/18 日漫