热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

VueSocket.io源码解读

背景有一个项目,今年12月份开始重构,项目涉及到了socket。但是socket用的是以前一个开发人员封装的包(这个一直被当前的成员吐槽为

背景

有一个项目,今年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] = valuereturn 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 instancethis.$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实例,也可以是一个urlif(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// 但是下面都是箭头函数了,我觉得没必要,毕竟箭头函数会自动绑定父级上下文的thislet _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中的mutationsfor(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和事件全等,那就发起这个actionif(action === camelcased) this.store.dispatch(namespaced, payload)}}

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

最后

  • 首先,这个源码是不是略有点简单,哈哈哈,不过,能给你们一些帮助,我觉得也挺好的
  • 然后,就是如果上面我说的有不是很对的,请大家去这里发issue或者直接评论吧
  • 最后,源码的详细的注释在这里,欢迎大家提issue,如果能star和fork就更好了。以后我尽量更新自己阅读源码的感悟,大家一起学习。



推荐阅读
  • DAO(Data Access Object)模式是一种用于抽象和封装所有对数据库或其他持久化机制访问的方法,它通过提供一个统一的接口来隐藏底层数据访问的复杂性。 ... [详细]
  • 更新vuex的数据为什么用mutation?
    更新vuex的数据为什么用mutation?,Go语言社区,Golang程序员人脉社 ... [详细]
  • 深入解析 Lifecycle 的实现原理
    本文将详细介绍 Android Jetpack 中 Lifecycle 组件的实现原理,帮助开发者更好地理解和使用 Lifecycle,避免常见的内存泄漏问题。 ... [详细]
  • 阿里面试题解析:分库分表后的无限扩容瓶颈与解决方案
    本文探讨了在分布式系统中,分库分表后的无限扩容问题及其解决方案。通过分析不同阶段的服务架构演变,提出了单元化作为解决数据库连接数过多的有效方法。 ... [详细]
  • java解析json转Map前段时间在做json报文处理的时候,写了一个针对不同格式json转map的处理工具方法,总结记录如下:1、单节点单层级、单节点多层级json转mapim ... [详细]
  • vue引入echarts地图的四种方式
    一、vue中引入echart1、安装echarts:npminstallecharts--save2、在main.js文件中引入echarts实例:  Vue.prototype.$echartsecharts3、在需要用到echart图形的vue文件中引入:   importechartsfrom"echarts";4、如果用到map(地图),还 ... [详细]
  • iOS 不定参数 详解 ... [详细]
  • 包含phppdoerrorcode的词条 ... [详细]
  • 第十九天 - 类的约束、异常处理与日志记录
    本文介绍了如何通过类的约束来确保代码的一致性,以及如何使用异常处理和日志记录来提高代码的健壮性和可维护性。具体包括抛出异常、使用抽象类和方法,以及异常处理和日志记录的详细示例。 ... [详细]
  • 本文节选自《NLTK基础教程——用NLTK和Python库构建机器学习应用》一书的第1章第1.2节,作者Nitin Hardeniya。本文将带领读者快速了解Python的基础知识,为后续的机器学习应用打下坚实的基础。 ... [详细]
  • 本文详细介绍了Java反射机制的基本概念、获取Class对象的方法、反射的主要功能及其在实际开发中的应用。通过具体示例,帮助读者更好地理解和使用Java反射。 ... [详细]
  • 本文将带你快速了解 SpringMVC 框架的基本使用方法,通过实现一个简单的 Controller 并在浏览器中访问,展示 SpringMVC 的强大与简便。 ... [详细]
  • IOS Run loop详解
    为什么80%的码农都做不了架构师?转自http:blog.csdn.netztp800201articledetails9240913感谢作者分享Objecti ... [详细]
  • 网站访问全流程解析
    本文详细介绍了从用户在浏览器中输入一个域名(如www.yy.com)到页面完全展示的整个过程,包括DNS解析、TCP连接、请求响应等多个步骤。 ... [详细]
  • 本文详细介绍了 PHP 中对象的生命周期、内存管理和魔术方法的使用,包括对象的自动销毁、析构函数的作用以及各种魔术方法的具体应用场景。 ... [详细]
author-avatar
手机用户2502883501
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有