Vue-Socket.io源碼閱讀

背景

有一個項目,今年12月份開始重構(gòu)泊愧,項目涉及到了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)绻褂眠@個庫的時候,一般是這樣寫的代碼(上圖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的時候税课,將在created時監(jiān)聽好的socket事件闲延,全部取消監(jiān)聽
     * delete this.$option.sockets的某個屬性時痊剖,就會將取消該信號的監(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í)例中的$options.sockets的值先緩存下來陆馁,再將$options.sockets指向一個proxy對象,這個proxy對象會攔截外界對它的賦值和刪除屬性操作侍匙。這里賦值的時候氮惯,鍵就是socket事件,值就是回調(diào)函數(shù)想暗。賦值時妇汗,就會監(jiān)聽該事件,然后將回調(diào)函數(shù)说莫,放進(jìn)該socket事件對應(yīng)的回調(diào)數(shù)組里杨箭。刪除時,就是取消監(jiān)聽該事件了储狭,將賦值時壓進(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)事件時也要把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弟跑,對吧。這個時候判斷下防症,拋出一個error提醒下也好孟辑,不過應(yīng)該也沒人這么無聊吧,2333蔫敲。然后如果傳入了store饲嗽,也掛在對象實(shí)例上吧。最后就啟動監(jiān)聽事件啦奈嘿。我們看看onEvent的邏輯

    onEvent(){
        // 監(jiān)聽服務(wù)端發(fā)來的事件貌虾,packet.data是一個數(shù)組
        // 第一項是事件,第二個是服務(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)系作者
  • 序言:七十年代末种蘸,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子竞膳,更是在濱河造成了極大的恐慌航瞭,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,406評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坦辟,死亡現(xiàn)場離奇詭異刊侯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)锉走,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,395評論 3 398
  • 文/潘曉璐 我一進(jìn)店門滨彻,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人挪蹭,你說我怎么就攤上這事亭饵。” “怎么了梁厉?”我有些...
    開封第一講書人閱讀 167,815評論 0 360
  • 文/不壞的土叔 我叫張陵辜羊,是天一觀的道長。 經(jīng)常有香客問我词顾,道長八秃,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 59,537評論 1 296
  • 正文 為了忘掉前任肉盹,我火速辦了婚禮昔驱,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘垮媒。我一直安慰自己舍悯,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,536評論 6 397
  • 文/花漫 我一把揭開白布睡雇。 她就那樣靜靜地躺著萌衬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪它抱。 梳的紋絲不亂的頭發(fā)上秕豫,一...
    開封第一講書人閱讀 52,184評論 1 308
  • 那天,我揣著相機(jī)與錄音,去河邊找鬼混移。 笑死祠墅,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的歌径。 我是一名探鬼主播毁嗦,決...
    沈念sama閱讀 40,776評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼回铛!你這毒婦竟也來了狗准?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,668評論 0 276
  • 序言:老撾萬榮一對情侶失蹤茵肃,失蹤者是張志新(化名)和其女友劉穎腔长,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體验残,經(jīng)...
    沈念sama閱讀 46,212評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡捞附,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,299評論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了您没。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片鸟召。...
    茶點(diǎn)故事閱讀 40,438評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖紊婉,靈堂內(nèi)的尸體忽然破棺而出药版,到底是詐尸還是另有隱情,我是刑警寧澤喻犁,帶...
    沈念sama閱讀 36,128評論 5 349
  • 正文 年R本政府宣布槽片,位于F島的核電站,受9級特大地震影響肢础,放射性物質(zhì)發(fā)生泄漏还栓。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,807評論 3 333
  • 文/蒙蒙 一传轰、第九天 我趴在偏房一處隱蔽的房頂上張望剩盒。 院中可真熱鬧,春花似錦慨蛙、人聲如沸辽聊。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,279評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽跟匆。三九已至,卻和暖如春通砍,著一層夾襖步出監(jiān)牢的瞬間玛臂,已是汗流浹背烤蜕。 一陣腳步聲響...
    開封第一講書人閱讀 33,395評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留迹冤,地道東北人讽营。 一個月前我還...
    沈念sama閱讀 48,827評論 3 376
  • 正文 我出身青樓,卻偏偏與公主長得像泡徙,于是被迫代替她去往敵國和親橱鹏。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,446評論 2 359

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