Vue-Socket.io源码解读

背景

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

开始

  • 文件架构

文件架构咱们主要看src下的三个文件,能够看出该库是用了观察者模式git

  • 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', {})github

// ?就是在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。最后将缓存的值,依次赋值回去,那么以下图的写法就会监听到事件并执行回调函数了:vuex

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的逻辑socket

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或者直接评论吧
  • 最后,源码的详细的注释在这里,欢迎你们提issue,若是能star和fork就更好了。之后我尽可能更新本身阅读源码的感悟,你们一块儿学习。
相关文章
相关标签/搜索