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 相关文章推荐
Add Formatted Text to a Word Document
Jun 15 Javascript
javascript中最常用的继承模式 组合继承
Aug 12 Javascript
jquery解决图片路径不存在执行替换路径
Feb 06 Javascript
使用jQuery validate 验证注册表单实例演示
Mar 25 Javascript
文本框中禁止非数字字符输入比如手机号码、邮编
Aug 19 Javascript
jQuery产品间断向下滚动效果核心代码
May 08 Javascript
DOM 事件流详解
Jan 20 Javascript
jquery实现漂亮的二级下拉菜单代码
Aug 26 Javascript
easyui tree带checkbox实现单选的简单实例
Nov 07 Javascript
vue2.0开发实践总结之入门篇
Dec 06 Javascript
js实现适配移动端的拖动效果
Jan 13 Javascript
利用JavaScript为句子加标题的3种方法示例
Jan 05 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
php根据一个给定范围和步进生成数组的方法
2015/06/19 PHP
PHP里的$_GET数组介绍
2019/03/22 PHP
Thinkphp自定义生成缩略图尺寸的方法
2019/08/05 PHP
JavaScript中的关键字"VAR"使用详解 分享
2013/07/31 Javascript
使用indexOf等在JavaScript的数组中进行元素查找和替换
2013/09/18 Javascript
Firefox中通过JavaScript复制数据到剪贴板(Copy to Clipboard 跨浏览器版)
2013/11/22 Javascript
JavaScript判断访问的来源是手机还是电脑,用的哪种浏览器
2013/12/12 Javascript
jQuery使用empty()方法删除元素及其所有子元素的方法
2015/03/26 Javascript
不能不知道的10个angularjs英文学习网站
2016/03/23 Javascript
Javascript实现汉字和拼音互转的终极方案
2016/10/19 Javascript
jQuery实现字符串全部替换的方法【推荐】
2017/03/09 Javascript
Bootstrap Table使用整理(五)之分页组合查询
2017/06/09 Javascript
解决jquery的ajax调取后端数据成功却渲染失败的问题
2018/08/08 jQuery
vue 本地环境跨域请求proxyTable的方法
2018/09/19 Javascript
JS实现数组深拷贝的方法分析
2019/03/06 Javascript
9种python web 程序的部署方式小结
2014/06/30 Python
Python学习入门之区块链详解
2017/07/25 Python
深入理解Python中的*重复运算符
2017/10/28 Python
python reduce 函数使用详解
2017/12/05 Python
Python sklearn KFold 生成交叉验证数据集的方法
2018/12/11 Python
Python图像处理实现两幅图像合成一幅图像的方法【测试可用】
2019/01/04 Python
python Django 创建应用过程图示详解
2019/07/29 Python
Python代码生成视频的缩略图的实例讲解
2019/12/22 Python
Python实现文件压缩和解压的示例代码
2020/08/12 Python
Python基于Socket实现简易多人聊天室的示例代码
2020/11/29 Python
Flask中jinja2的继承实现方法及实例
2021/03/03 Python
美国最流行的男士时尚网站:Touch of Modern
2018/02/05 全球购物
北美最大的参茸药食商城:德成行
2020/12/06 全球购物
跟单文员的岗位职责
2013/11/14 职场文书
学习全国两会精神心得体会范文
2014/03/17 职场文书
文明班集体申报材料
2014/05/23 职场文书
敬老院献爱心活动总结
2014/07/08 职场文书
2014年招生工作总结
2014/11/26 职场文书
幼儿园教师求职信
2015/03/20 职场文书
加强党性修养心得体会
2016/01/21 职场文书
springboot项目以jar包运行的操作方法
2021/06/30 Java/Android