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 相关文章推荐
精心挑选的12款优秀的基于jQuery的手风琴效果插件和教程
Aug 22 Javascript
基于JavaScript 类的使用详解
May 07 Javascript
使用非html5实现js板连连看游戏示例代码
Sep 22 Javascript
js加入收藏以及使用Jquery更改透明度
Jan 26 Javascript
jQuery避免$符和其他JS库冲突的方法对比
Feb 20 Javascript
javascript封装 Cookie 应用接口
Aug 07 Javascript
原生JavaScript实现瀑布流布局
Jun 28 Javascript
jquery实现页面常用的返回顶部效果
Mar 04 Javascript
基于jQuery和Bootstrap框架实现仿知乎前端动态列表效果
Nov 09 Javascript
js 颜色选择插件
Jan 23 Javascript
详解小程序原生使用ES7 async/await语法
Aug 06 Javascript
Vue 实例中使用$refs的注意事项
Jan 29 Vue.js
原生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
颠覆常识!无色透明的咖啡诞生了(中日双语)
2021/03/03 咖啡文化
PHP使用DOMDocument类生成HTML实例(包含常见标签元素)
2014/06/25 PHP
PHP实现在线阅读PDF文件的方法
2015/06/23 PHP
PHP+AJAX实现投票功能的方法
2015/09/28 PHP
php array_walk 对数组中的每个元素应用用户自定义函数详解
2016/11/18 PHP
PHP会话控制实例分析
2016/12/24 PHP
jQuery遍历Form示例代码
2013/09/03 Javascript
JS小功能(checkbox实现全选和全取消)实例代码
2013/11/28 Javascript
利用js(jquery)操作Cookie的方法说明
2013/12/19 Javascript
javascript使用正则控制input输入框允许输入的值方法大全
2014/06/19 Javascript
jQuery使用drag效果实现自由拖拽div
2015/06/11 Javascript
jQuery插件jquery-barcode实现条码打印的方法
2015/11/25 Javascript
基于JavaScript实现图片点击弹出窗口而不是保存
2016/02/06 Javascript
基于jquery实现表格内容筛选功能实例解析
2016/05/09 Javascript
ionic实现带字的toggle滑动组件
2016/08/27 Javascript
基于vue+canvas的excel-like组件实例详解
2017/11/28 Javascript
Vue单页及多页应用全局配置404页面实践记录
2018/05/22 Javascript
解决vue 中 echart 在子组件中只显示一次的问题
2018/08/07 Javascript
Vue中component标签解决项目组件化操作
2020/09/04 Javascript
python3+PyQt5使用数据库窗口视图
2018/04/24 Python
python pandas库中DataFrame对行和列的操作实例讲解
2018/06/09 Python
python查看模块,对象的函数方法
2018/10/16 Python
24式加速你的Python(小结)
2019/06/13 Python
Django自定义全局403、404、500错误页面的示例代码
2020/03/08 Python
python中threading开启关闭线程操作
2020/05/02 Python
俄罗斯天然和有机产品、健康生活网上商店:Fitomarket.ru
2020/10/09 全球购物
瑞典多品牌连锁店:Johnells
2021/01/13 全球购物
期末总结的个人自我评价
2013/11/02 职场文书
如何打造一封优秀的留学推荐信
2014/01/25 职场文书
《阳光》教学反思
2014/02/23 职场文书
2014年财务科工作总结
2014/11/11 职场文书
郭明义电影观后感
2015/06/08 职场文书
搞笑欢迎词大全
2015/09/30 职场文书
话题作文之诚信
2019/11/28 职场文书
详解vue身份认证管理和租户管理
2021/05/25 Vue.js
如何让你的Nginx支持分布式追踪详解
2022/07/07 Servers