摘要:刪除時(shí),就是取消監(jiān)聽該事件了,將賦值時(shí)壓進(jìn)回調(diào)數(shù)組的那個(gè)回調(diào)函數(shù),刪除,表示,我不監(jiān)聽了。這段代碼先判斷中是否之前已經(jīng)存儲過了該事件,如果沒有,初始化該事件對應(yīng)的值為空數(shù)組,然后將當(dāng)前的回調(diào)函數(shù),壓進(jìn)去,反之,直接壓進(jìn)去。
背景
有一個(gè)項(xiàng)目,今年12月份開始重構(gòu),項(xiàng)目涉及到了socket。但是socket用的是以前一個(gè)開發(fā)人員封裝的包(這個(gè)一直被當(dāng)前的成員吐槽為什么不用已經(jīng)千錘百煉的輪子)。因此,趁著這個(gè)重構(gòu)的機(jī)會,將vue-socket.io引入,后端就用socket.io。我也好奇看了看vue-socket.io的源碼(我不會說是因?yàn)檫@個(gè)庫的文檔實(shí)在太簡略了,我為了穩(wěn)點(diǎn)去看源碼了解該怎么用)
開始文件架構(gòu)
我們主要看src下的三個(gè)文件,可以看出該庫是用了觀察者模式
Main.js
// 這里創(chuàng)建一個(gè)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)绻褂眠@個(gè)庫的時(shí)候,一般是這樣寫的代碼(上圖2)。上圖一的connection和store就分別是圖二的后兩個(gè)參數(shù)。意思分別為socket連接的url和vuex的store啦。圖一就是將這兩個(gè)參數(shù)傳進(jìn)Observer,新建了一個(gè)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的某個(gè)屬性時(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指向一個(gè)proxy對象,這個(gè)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ù)組的那個(gè)回調(diào)函數(shù),刪除,表示,我不監(jiān)聽了。這樣寫法,其實(shí)就跟vue的響應(yīng)式一個(gè)道理。也因此,我們就可以動態(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主要是寫了一個(gè)Emitter對象,該對象提供了三個(gè)方法:
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用一個(gè)map來存儲事件以及它對應(yīng)的回調(diào)事件數(shù)組。這段代碼先判斷map中是否之前已經(jīng)存儲過了該事件,如果沒有,初始化該事件對應(yīng)的值為空數(shù)組,然后將當(dāng)前的回調(diào)函數(shù),壓進(jìn)去,反之,直接壓進(jìn)去。
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ù)組覆蓋原來的那個(gè)就可以了
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) { // 這里很明白吧,就是判斷這個(gè)connection是什么類型 // 這里的處理就是你可以傳入一個(gè)連接好的socket實(shí)例,也可以是一個(gè)url if(typeof connection == "string"){ this.Socket = Socket(connection); }else{ this.Socket = connection } // 如果有傳進(jìn)vuex的store可以響應(yīng)在store中寫的mutations和actions // 這里只是掛載在這個(gè)oberver實(shí)例上 if(store) this.store = store; // 監(jiān)聽,啟動! this.onEvent() }
這個(gè)Observer.js里也主要是寫了一個(gè)Observer的class,以上是它的構(gòu)造函數(shù),構(gòu)造函數(shù)第一件事是判斷connection是不是字符串,如果是就構(gòu)建一個(gè)socket實(shí)例,如果不是,就大概是個(gè)socket的實(shí)例了,然后直接掛載在它的對象實(shí)例上。其實(shí)這里我覺得可以參數(shù)檢查嚴(yán)格點(diǎn), 比如字符串被人搞怪地可能會傳入一個(gè)非法的url,對吧。這個(gè)時(shí)候判斷下,拋出一個(gè)error提醒下也好,不過應(yīng)該也沒人這么無聊吧,2333。然后如果傳入了store,也掛在對象實(shí)例上吧。最后就啟動監(jiān)聽事件啦。我們看看onEvent的邏輯
onEvent(){ // 監(jiān)聽服務(wù)端發(fā)來的事件,packet.data是一個(gè)數(shù)組 // 第一項(xiàng)是事件,第二個(gè)是服務(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)聽到下面的信號? // 然后這里用一個(gè)變量暫存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這個(gè)函數(shù)了,監(jiān)聽到事件后,將數(shù)據(jù)拆包,然后通知執(zhí)行回調(diào)和傳遞給store。大體的邏輯是這樣子。然后這代碼實(shí)現(xiàn)有兩部分,第一部分和第二部分邏輯基本一樣。只是分開寫。(其實(shí)我也不是很懂啦,如果很有必要的話,我猜第一部分的寫法還監(jiān)聽不了第二部分的事件吧,所以要另外監(jiān)聽)。最后只剩下一個(gè)passToStore了,其實(shí)也很容易懂
passToStore(event, payload){ // 如果事件不是以SOCKET_開頭的就不用管了 if(!event.startsWith("SOCKET_")) return // 這里遍歷vuex的store中的mutations for(let namespaced in this.store._mutations) { // 下面的操作是因?yàn)?,如果store中有module是開了namespaced的,會在mutation的名字前加上 xxx/ // 這里將mutation的名字拿出來 let mutation = namespaced.split("/").pop() // 如果名字和事件是全等的,那就發(fā)起一個(gè)commit去執(zhí)行這個(gè)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])|[Ws_]+(w)/g, (match, p1, p2) => p2 ? p2.toUpperCase() : p1.toLowerCase()) // 如果action和事件全等,那就發(fā)起這個(gè)action if(action === camelcased) this.store.dispatch(namespaced, payload) } }
passToStore嘛其實(shí)就是做兩個(gè)事情,一個(gè)是獲取與該事件對應(yīng)的mutation,然后發(fā)起一個(gè)commit,一個(gè)是獲取與該事件對應(yīng)的action,然后dispatch。只是這里的實(shí)現(xiàn)對mutations和actions的命名有了要求,比如mutations的命名一定得是SOCKET_開頭,action就是一個(gè)得socket_開頭,然后還得是駝峰式命名。
最后首先,這個(gè)源碼是不是略有點(diǎn)簡單,哈哈哈,不過,能給你們一些幫助,我覺得也挺好的
然后,就是如果上面我說的有不是很對的,請大家去這里發(fā)issue或者直接評論吧
最后,源碼的詳細(xì)的注釋在這里,歡迎大家提issue,如果能star和fork就更好了。以后我盡量更新自己閱讀源碼的感悟,大家一起學(xué)習(xí)。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://systransis.cn/yun/92252.html
摘要:項(xiàng)目中在瀏覽器下報(bào)缺少,缺少今天用打開了我們的項(xiàng)目,結(jié)果一片空白于是趕緊打開一探究竟行,是啥呢,點(diǎn)過去瞅瞅好吧,去里把注釋掉錯(cuò)誤信息變成了缺少經(jīng)排查,是導(dǎo)致的這條報(bào)錯(cuò)那么,和就是罪魁禍?zhǔn)琢恕? Vue-Cli項(xiàng)目中vue-socketio.js在IE瀏覽器下報(bào)SCRIPT1003: 缺少 : ,vue-charts 缺少) 今天用ie打開了我們的Vue項(xiàng)目,結(jié)果一片空白 于是趕緊打開F1...
摘要:一寫在前面如何實(shí)現(xiàn)文章的實(shí)時(shí)保存一般寫文章的寫博客的網(wǎng)站都會有這個(gè)功能點(diǎn),這樣保證了用戶在不小心退出的情況下數(shù)據(jù)的保存下來,這樣的交互比較符合用戶的使用心理學(xué)。 一、寫在前面 如何實(shí)現(xiàn)文章的實(shí)時(shí)保存?一般寫文章的寫博客的網(wǎng)站都會有這個(gè)功能點(diǎn),這樣保證了用戶在不小心退出的情況下數(shù)據(jù)的保存下來,這樣的交互比較符合用戶的使用心理學(xué)。對于用戶來說這是一個(gè)非常實(shí)用的功能,作為一個(gè)博客來說,有這個(gè)...
摘要:前言這個(gè)輪子已經(jīng)有很多人造過了,為了不重復(fù)造輪子,我將本項(xiàng)目以三階段實(shí)現(xiàn)大家可以在中的查看純前端后端前端后端前端希望能給大家一個(gè)漸進(jìn)學(xué)習(xí)的經(jīng)驗(yàn)。 前言 Vue+Socket.io這個(gè)輪子已經(jīng)有很多人造過了,為了不重復(fù)造輪子,我將本項(xiàng)目以三階段實(shí)現(xiàn)(大家可以在github中的Releases查看): 純前端(Vuex) 后端+前端(JavaScript) 后端+前端(TypeScrip...
摘要:在學(xué)習(xí)源碼的過程中,給我?guī)椭畲蟮木褪沁@個(gè)系列文章,于是決定基于這個(gè)系列文章談一下自己的理解。到此為止,首次渲染就完成啦總結(jié)從啟動到元素渲染到頁面,并不像看起來這么簡單,中間經(jīng)歷了復(fù)雜的層級調(diào)用。 前言 React 是一個(gè)十分龐大的庫,由于要同時(shí)考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個(gè)非常艱辛的過...
閱讀 1715·2021-11-02 14:47
閱讀 3661·2019-08-30 15:44
閱讀 1350·2019-08-29 16:42
閱讀 1743·2019-08-26 13:53
閱讀 945·2019-08-26 10:41
閱讀 3476·2019-08-23 17:10
閱讀 615·2019-08-23 14:24
閱讀 1729·2019-08-23 11:59