Vue-Socket.io源碼閱讀

背景

有一個項(xiàng)目,今年12月份開始重構(gòu),項(xiàng)目涉及到了socket。但是socket用的是以前一個開發(fā)人員封裝的包(這個一直被當(dāng)前的成員吐槽為什么不用已經(jīng)千錘百煉的輪子)。因此,趁著這個重構(gòu)的機(jī)會,將vue-socket.io引入,后端就用socket.io。我也好奇看了看vue-socket.io的源碼(我不會說是因?yàn)檫@個庫的文檔實(shí)在太簡略了,我為了穩(wěn)點(diǎn)去看源碼了解該怎么用)

開始

  • 文件架構(gòu)


    文件架構(gòu)

    我們主要看src下的三個文件,可以看出該庫是用了觀察者模式

  • Main.js
// 這里創(chuàng)建一個observe對象,具體做了什么可以看Observer.js文件
let observer = new Observer(connection, store)

// 將socket掛載到了vue的原型上,然后就可以
// 在vue實(shí)例中就可以this.$socket.emit('xxx', {})
Vue.prototype.$socket = observer.Socket;
import store from './yourstore'
Vue.use(VueSocketio, socketio('http://socketserver.com:1923'), store);

我們?nèi)绻褂眠@個庫的時(shí)候,一般是這樣寫的代碼(上圖2)。上圖一的connection和store就分別是圖二的后兩個參數(shù)。意思分別為socket連接的url和vuex的store啦。圖一就是將這兩個參數(shù)傳進(jìn)Observer,新建了一個observe對象,然后將observe對象的socket屬性掛載在Vue原型上。那么我們在Vue的實(shí)例中就可以直接 this.$sockets.emit('xxx', {})

// ??就是在vue實(shí)例的生命周期做一些操作
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的時(shí)候,將在created時(shí)監(jiān)聽好的socket事件,全部取消監(jiān)聽
     * delete this.$option.sockets的某個屬性時(shí),就會將取消該信號的監(jiān)聽
     */
    beforeDestroy(){
        let sockets = this.$options['sockets']

        if(sockets){
            Object.keys(sockets).forEach((key) => {
                delete this.$options.sockets[key]
            });
        }
    }

下面就是在Vue實(shí)例的生命周期做一些操作。創(chuàng)建的時(shí)候,將實(shí)例中的$options.sockets的值先緩存下來,再將$options.sockets指向一個proxy對象,這個proxy對象會攔截外界對它的賦值和刪除屬性操作。這里賦值的時(shí)候,鍵就是socket事件,值就是回調(diào)函數(shù)。賦值時(shí),就會監(jiān)聽該事件,然后將回調(diào)函數(shù),放進(jìn)該socket事件對應(yīng)的回調(diào)數(shù)組里。刪除時(shí),就是取消監(jiān)聽該事件了,將賦值時(shí)壓進(jìn)回調(diào)數(shù)組的那個回調(diào)函數(shù),刪除,表示,我不監(jiān)聽了。這樣寫法,其實(shí)就跟vue的響應(yīng)式一個道理。也因此,我們就可以動態(tài)地添加和移除監(jiān)聽socket事件了,比如this.$option.sockets.xxx = () => ()delete this.$option.sockets.xxx。最后將緩存的值,依次賦值回去,那么如下圖的寫法就會監(jiān)聽到事件并執(zhí)行回調(diào)函數(shù)了:

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) {
    // 回調(diào)函數(shù)類型是回調(diào)函數(shù)才對
    if(typeof callback == 'function'){
        // 這里就很常見的寫法了,判斷map中是否已經(jīng)注冊過該事件了
        // 如果沒有,就初始化該事件映射的值為空數(shù)組,方便以后直接存入回調(diào)函數(shù)
        // 反之,直接將回調(diào)函數(shù)放入數(shù)組即可
        this.listeners.has(label) || this.listeners.set(label, []);
        this.listeners.get(label).push({callback: callback, vm: vm});

        return true
    }

    return false
}

其實(shí)很常規(guī)啦,實(shí)現(xiàn)發(fā)布訂閱者模式或者觀察者模式代碼的同學(xué)都很清楚這段代碼的意思。Emiiter用一個map來存儲事件以及它對應(yīng)的回調(diào)事件數(shù)組。這段代碼先判斷map中是否之前已經(jīng)存儲過了該事件,如果沒有,初始化該事件對應(yīng)的值為空數(shù)組,然后將當(dāng)前的回調(diào)函數(shù),壓進(jìn)去,反之,直接壓進(jìn)去。

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;

這里也很簡單啦,獲取該事件對應(yīng)的回調(diào)數(shù)組。如果不為空,就去尋找需要移除的回調(diào),找到后,直接刪除,然后將新的回調(diào)數(shù)組覆蓋原來的那個就可以了

emit

if (listeners && listeners.length) {
    listeners.forEach((listener) => {
        listener.callback.call(listener.vm,...args)
    });
    return true;
}
return false;

這里就是監(jiān)聽到事件后,執(zhí)行該事件對應(yīng)的回調(diào)函數(shù),注意這里的call,因?yàn)楸O(jiān)聽到事件后我們可能要修改下vue實(shí)例的數(shù)據(jù)或者調(diào)用一些方法,用過vue的同學(xué)都知道我們都是this.xxx來調(diào)用的,所以一定得將回調(diào)函數(shù)的this指向vue實(shí)例,這也是為什么存回調(diào)事件時(shí)也要把vue實(shí)例存下來的原因。

  • Observer.js
constructor(connection, store) {
    // 這里很明白吧,就是判斷這個connection是什么類型
    // 這里的處理就是你可以傳入一個連接好的socket實(shí)例,也可以是一個url
    if(typeof connection == 'string'){
        this.Socket = Socket(connection);
    }else{
        this.Socket = connection
    }

    // 如果有傳進(jìn)vuex的store可以響應(yīng)在store中寫的mutations和actions
    // 這里只是掛載在這個oberver實(shí)例上
    if(store) this.store = store;

    // 監(jiān)聽,啟動!
    this.onEvent()

}

這個Observer.js里也主要是寫了一個Observer的class,以上是它的構(gòu)造函數(shù),構(gòu)造函數(shù)第一件事是判斷connection是不是字符串,如果是就構(gòu)建一個socket實(shí)例,如果不是,就大概是個socket的實(shí)例了,然后直接掛載在它的對象實(shí)例上。其實(shí)這里我覺得可以參數(shù)檢查嚴(yán)格點(diǎn), 比如字符串被人搞怪地可能會傳入一個非法的url,對吧。這個時(shí)候判斷下,拋出一個error提醒下也好,不過應(yīng)該也沒人這么無聊吧,2333。然后如果傳入了store,也掛在對象實(shí)例上吧。最后就啟動監(jiān)聽事件啦。我們看看onEvent的邏輯

    onEvent(){
        // 監(jiān)聽服務(wù)端發(fā)來的事件,packet.data是一個數(shù)組
        // 第一項(xiàng)是事件,第二個是服務(wù)端傳來的數(shù)據(jù)
        // 然后用emit通知訂閱了該信號的回調(diào)函數(shù)執(zhí)行
        // 如果有傳入了vuex的store,將該事件和數(shù)據(jù)傳入passToStore,執(zhí)行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)])
        };

        // 這里跟上面意思應(yīng)該是一樣的,我很好奇為什么要分開寫,難道上面的寫法不會監(jiān)聽到下面的信號?
        // 然后這里用一個變量暫存this
        // 但是下面都是箭頭函數(shù)了,我覺得沒必要,畢竟箭頭函數(shù)會自動綁定父級上下文的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)
                })
            })
    }

這里就是有點(diǎn)類似重載onevent這個函數(shù)了,監(jiān)聽到事件后,將數(shù)據(jù)拆包,然后通知執(zhí)行回調(diào)和傳遞給store。大體的邏輯是這樣子。然后這代碼實(shí)現(xiàn)有兩部分,第一部分和第二部分邏輯基本一樣。只是分開寫。(其實(shí)我也不是很懂啦,如果很有必要的話,我猜第一部分的寫法還監(jiān)聽不了第二部分的事件吧,所以要另外監(jiān)聽)。最后只剩下一個passToStore了,其實(shí)也很容易懂

 passToStore(event, payload){
     // 如果事件不是以SOCKET_開頭的就不用管了
     if(!event.startsWith('SOCKET_')) return

     // 這里遍歷vuex的store中的mutations
     for(let namespaced in this.store._mutations) {
         // 下面的操作是因?yàn)椋绻鹲tore中有module是開了namespaced的,會在mutation的名字前加上 xxx/
         // 這里將mutation的名字拿出來
         let mutation = namespaced.split('/').pop()
         // 如果名字和事件是全等的,那就發(fā)起一個commit去執(zhí)行這個mutation
         // 也因此,mutation的名字一定得是 SOCKET_開頭的了
         if(mutation === event.toUpperCase()) this.store.commit(namespaced, payload)
     }
     // 這里類似上面
     for(let namespaced in this.store._actions) {
         let action = namespaced.split('/').pop()

         // 這里強(qiáng)制要求了action的名字要以 socket_ 開頭
         if(!action.startsWith('socket_')) continue

         // 這里就是將事件轉(zhuǎn)成駝峰式
         let camelcased = 'socket_'+event
                 .replace('SOCKET_', '')
                 .replace(/^([A-Z])|[\W\s_]+(\w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase())

         // 如果action和事件全等,那就發(fā)起這個action
         if(action === camelcased) this.store.dispatch(namespaced, payload)
     }
 }

passToStore嘛其實(shí)就是做兩個事情,一個是獲取與該事件對應(yīng)的mutation,然后發(fā)起一個commit,一個是獲取與該事件對應(yīng)的action,然后dispatch。只是這里的實(shí)現(xiàn)對mutations和actions的命名有了要求,比如mutations的命名一定得是SOCKET_開頭,action就是一個得socket_開頭,然后還得是駝峰式命名。

最后

  • 首先,這個源碼是不是略有點(diǎn)簡單,哈哈哈,不過,能給你們一些幫助,我覺得也挺好的
  • 然后,就是如果上面我說的有是很對的,請大家去這里發(fā)issue或者直接評論吧
  • 最后,源碼的詳細(xì)的注釋在這里,歡迎大家提issue,如果能star和fork就更好了。以后我盡量更新自己閱讀源碼的感悟,大家一起學(xué)習(xí)。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 229,117評論 6 537
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 98,860評論 3 423
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 177,128評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,291評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 72,025評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 55,421評論 1 324
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼。 笑死,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,477評論 3 444
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 42,642評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 49,177評論 1 335
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 40,970評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 43,157評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,717評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 44,410評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,821評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,053評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,896評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 48,157評論 2 375

推薦閱讀更多精彩內(nèi)容