關於文章
最近在提升個人技能的同時,決定把自己為數不多的沉淀記錄下來,讓自己理解的更加深刻,同時也歡迎各位看官指出不足之處。
隨着node.js的盛行,引領着Javascript上天下地無所不能啊,本人確確實實的一個前端的忠實粉絲,於是乎......不能自拔...node的異步回調機制有效的提高了非密集型程序的高並發、速度快、性能優的需求同時,也飽受各大廠商的青睞,正是node將javascript又一次推向了熱潮,於是小白又要埋頭苦讀,在各位大神和看官指引的道路下前進。
這篇文章是我的第一篇文章,很早就下定決心要記錄和分享一些自己對已掌握的知識的一些理解(其實真心覺得將自己學到東西以文章的形式分享給大家,話風通俗易懂,不僅鞏固了自己的知識體系,還會 潤物無聲),可遲遲沒有勇氣和信心。
這篇文章主要記錄一下自己前幾天手寫的一個兼容node環境、瀏覽器環境、還支持Vue的...庫。event-mange( npm倉庫的名字)。node下的EventEmitter想必大家都很熟悉了,在思考過后決定自己也要產出一個類似的事件管理,不僅能在node下,還可以在瀏覽器端以 script標簽 的方式引入使用,還有CMD、AMD下...於是,本小白開始動工了。
關於事件
在我們使用javascript開發時,我們會經常用到很多事件,如點擊、鍵盤、鼠標等等,這些物理性的事件。而我們今天所說的我稱之為事件的,是另一種形式的事件,訂閱---發布,又叫做觀察者模式,他定義了一對多的依賴關系,當一個對象狀態發生改變時,所有依賴於它的對象都會收到通知,而在javascript中,一般習慣性的用事件模型來替代發布---訂閱模式。
列舉一個生活中的例子來幫助大家理解這一種模式。炎熱的夏天,媽媽燒好了飯盛上桌,冒着熱氣,這時媽媽喊小明吃飯(小明在旁邊的屋子里餓着肚子大吉大利晚上吃雞...),小明出來一看,跟媽媽說,等一會 ‘飯涼了’ 再叫我,太燙了...十分鍾后...媽媽喊你 ‘飯涼了’,快來吃飯,而這時小明聽到了媽媽的喊話說 ‘飯涼了’,便快速的出來吃完了。這個例子,就是以上介紹的訂閱---發布模式。例子中的小明就是訂閱者(訂閱的是 ‘飯涼了’),而媽媽則是發布者(將信號 ‘飯涼了’ 發布出去)。
使用訂閱---發布模式的有着顯而易見的優點:訂閱者不用每時每刻都詢問發布者飯是否涼了,在合適的事件點,發布者會通知這些訂閱者,告訴他們飯涼了,他們可以過來吃了。這樣就不用把小明和媽媽強耦合在一起,當小明的弟弟妹妹都想在飯涼了在吃飯,只需告訴媽媽一聲。就像每個看官肯定都接觸過的一種訂閱---發布:DOM事件的綁定
document.body.addEventListener('click', function (e) {
console.log('我執行了...')
}, false)
回歸正題:
event-mange 通過訂閱-發布模式實現的
一步一步的實現
event-mange 模塊的主要方法:
- on:訂閱者,添加事件
- emit:發布者, 出發事件
- once: 訂閱者,添加只能監聽一次之后就失效的事件
- removeListener:刪除單個訂閱(事件)
- removeAllListener: 刪除單個事件類型的訂閱或刪除全部訂閱
- getListenerCount:獲得訂閱者的數量
event-mange 模塊的主要屬性:
- MaxEventListNum: 設置單個事件最多訂閱者數量(默認為10)
基本骨架
首先,我們希望通過 event.on , event.emit 來訂閱和發布,通過構造函數來創建一個event實例,而on,emit分別為這個實例的兩個方法, 同樣的,以上列出的所有主要方法,都是event的對象的原型方法。
function events () {};
// 列舉去我們想要實現的event對象的方法
event.prototype.on = function () {};
event.prototype.emit = function () {};
event.prototype.once = function () {};
event.prototype.removeListener = function () {};
event.prototype.removeAllListener = function () {};
event.prototype.getListenerCount = function () {};
似乎丟了什么,沒錯,是event對象我們上面列出來的MaxEventListNum屬性,我們給他補上
function event () {
//因為MaxEventListNum屬性是可以讓開發者設置的
//所以在沒有set的時候,我們將其設置為 undefind
this.MaxEventListNum = this.MaxEventListNum || undefined;
//如果沒有設置set,我們不能讓監聽數量無限大
//這樣有可能會造成內存溢出
//所以我們將默認數量設置為10(當然,設置成別的數量也是可以的)
this.defaultMaxEventListNum = 10;
}
到這里,基本上我們想實現的時間管理模塊屬性和方法的初態也就差不多了,也就是說,骨架出來了,我們就需要填飽他的代碼邏輯,讓他變的有血有肉(看似像個生命...)
值得思考的是,骨架我們構建完了,我們要做的是一個訂閱--發布模式,我們應該怎么去記住眾多的訂閱事件呢? 首先,對於一個訂閱,我們需要有一個訂閱的類型,也就是topic,針對此topic我們要把所有的訂閱此topic的事件都放在一起,對,可以選擇Array,初步的構造
event_list: {
topic1: [fn1, fn2, fn3 ...]
...
}
那么接下來我們將存放我們事件的event_list放入代碼中完善,作為event的屬性
function event () {
// 這里我們做一個簡單的判斷,以免一些意外的錯誤出現
if(!this.event_list) {
this.event_list = {};
}
this.MaxEventListNum = this.MaxEventListNum || undefined;
this.defaultMaxEventListNum = 10;
}
on 方法實現
event.prototype.on = function () {};
通過分析得出on方法首先應該接收一個訂閱的topic,其次是一個當此topic響應后觸發的callback方法
event.prototype.on = function (eventName, content) {};
eventName作為事件類型,將其作為event_list的一個屬性,所有的事件類型為eventName的監聽都push到eventName這個數組里面。
event.prototype.on = function (eventName, content) {
...
var _event, ctx;
_event = this.event_list;
// 再次判斷event_list是否存在,不存在則重新賦值
if (!_event) {
_event = this.event_list = {};
} else {
// 獲取當前eventName的監聽
ctx = this.event_list[eventName];
}
// 判斷是否有此監聽類型
// 如果不存在,則表示此事件第一次被監聽
// 將回調函數 content 直接賦值
if (!ctx) {
ctx = this.event_list[eventName] = content;
// 改變訂閱者數量
ctx.ListenerCount = 1;
} else if (isFunction(ctx)) {
// 判斷此屬性是否為函數(是函數則表示已經有且只有一個訂閱者)
// 將此eventName類型由函數轉變為數組
ctx = this.event_list[eventName] = [ctx, content];
// 此時訂閱者數量變為數組長度
ctx.ListenerCount = ctx.length;
} else if (isArray(ctx)) {
// 判斷是否為數組,如果是數組則直接push
ctx.push(content);
ctx.ListenerCount = ctx.length;
}
...
};
once 方法實現
event.prototype.once = function () {};
once方法對已訂閱事件只執行一次,需執行完后立即在event_list中相應的訂閱類型屬性中刪除該訂閱的回調函數,其存儲過程與on方法幾乎一致,同樣需要一個訂閱類型的topic,以及一個響應事件的回調 content
event.prototype.once = function (eventName, content) {};
在執行完本次事件回調后立即取消注冊此訂閱,而如果此時同一類型的事件注冊了多個監聽回調,我們無法准確的刪除當前once方法所注冊的監聽回調,所以通常我們采用的遍歷事件監聽隊列,找到相應的監聽回調然后將其刪除是行不通的。還好,偉大的javascript語言為我們提供了一個強大的閉包特性,通過閉包的方式來裝飾content,包裝成一個全新的函數。
events.prototype.once = function (event, content) {
...
// once和on的存儲事件回調機制相同
// dealOnce 函數 包裝函數
this.on(event, dealOnce(this, event, content));
...
}
// 包裝函數
function dealOnce(target, type, content) {
var flag = false;
// 通過閉包特性(會將函數外部引用保存在作用域中)
function packageFun() {
// 當此監聽回調被調用時,會先刪除此回調方法
this.removeListener(type, packageFun);
if (!flag) {
flag = true;
// 因為閉包,所以原監聽回調還會保留,所以還會執行
content.apply(target, arguments);
}
packageFun.content = content;
}
return packageFun;
}
once的實現其實將我們自己傳遞的回調函數做了二次封裝,再綁定上封裝后的函數,封裝的函數首先執行了removeListener()移除了回調函數與事件的綁定,然后才執行的回調函數
emit 方法實現
event.prototype.emit = function () {};
emit方法用來發布事件,驅動執行相應的事件監聽隊列中的監聽回調,故我們需要一個事件type的topic
event.prototype.emit = function (eventName[,message][,message1][,...]) {};
當然,發布事件是,也可以像該事件監聽者傳遞參數,數量不限,則會依次傳遞給所有的監聽回調
event.prototype.emit = function (eventName[,message]) {
var _event, ctx;
//除第一個參數eventNmae外,其他參數保存在一個數組里
var args = Array.prototype.slice.call(arguments, 1);
_event = this.event_list;
// 檢測存儲事件隊列是否存在
if (_event) {
// 如果存在,得到此監聽類型
ctx = this.event_list[eventName];
}
// 檢測此監聽類型的事件隊列
// 不存在則直接返回
if (!ctx) {
return false;
} else if (isFunction(ctx)) {
// 是番薯則直接執行,並將所有參數傳遞給此函數(回調函數)
ctx.apply(this, args);
} else if (isArray(ctx)) {
// 是數組則遍歷調用
for (var i = 0; i < ctx.length; i++) {
ctx[i].apply(this, args);
}
}
};
emit從理解程度上來說應該是更容易一些,只是從存儲事件的對象中找到相應類型的監聽事件隊列,然后執行隊列中的每一個回調
removeListener 方法實現
event.prototype.removeListener = function () {};
刪除某種監聽類型的某一個監聽回調,顯然,我們仍然需要一個事件type,以及一個監聽回調,當事件對列中的回調與該回調相同時,則移除
event.prototype.removeListener = function (eventName, content) {};
需要注意的是,如果我們確實存在要移除某個監聽事件的回調,在on方法時一定不要使用匿名函數作為回調,這樣會導致在removeListener是無法移除,因為在javascript中匿名函數是不相等的。
// 如果需要移除
// 錯誤
event.on('eatting', function (msg) {
});
// 正確
event.on('eatting', cb);
// 回調
function cb (msg) {
...
}
event.prototype.removeListener = function (eventName, content) {
var _event, ctx, index = 0;
_event = this.event_list;
if (!_event) {
return this;
} else {
ctx = this.event_list[eventName];
}
if (!ctx) {
return this;
}
// 如果是函數 直接delete
if (isFunction(ctx)) {
if (ctx === content) {
delete _event[eventName];
}
} else if (isArray(ctx)) {
// 如果是數組 遍歷
for (var i = 0; i < ctx.length; i++) {
if (ctx[i] === content) {
// 監聽回調相等
// 從該監聽回調的index開始,后面的回調依次覆蓋掉前面的回調
// 將最后的回調刪除
// 等價於直接將滿足條件的監聽回調刪除
this.event_list[eventName].splice(i - index, 1);
ctx.ListenerCount = ctx.length;
if (this.event_list[eventName].length === 0) {
delete this.event_list[eventName]
}
index++;
}
}
}
};
removeAllListener 方法實現
event.prototype.removeAllListener = function () {};
此方法有兩個用途,即實現當有參數事件類型eventName時,則刪除該類型的所有監聽(清空此事件的監聽回調隊列),當沒有參數時,則將所有類型的事件監聽對壘全部移除,還是比較好理解的直接上代碼
event.prototype.removeAllListener = function ([,eventName]) {
var _event, ctx;
_event = this.event_list;
if (!_event) {
return this;
}
ctx = this.event_list[eventName];
// 判斷是否有參數
if (arguments.length === 0 && (!eventName)) {
// 無參數
// 將key 轉成 數組 並遍歷
// 依次刪除所有的類型監聽
var keys = Object.keys(this.event_list);
for (var i = 0, key; i < keys.length; i++) {
key = keys[i];
delete this.event_list[key];
}
}
// 有參數 直接移除
if (ctx || isFunction(ctx) || isArray(ctx)) {
delete this.event_list[eventName];
} else {
return this;
}
};
其主要實現思路大致如上所述,貌似還漏了一些什么,哦,是對於是否超過艦艇數量的最大限制的處理
在on方法中
...
// 檢測回調隊列是否有maxed屬性以及是否為false
if (!ctx.maxed) {
//只有在是數組的情況下才會做比較
if (isArray(ctx)) {
var len = ctx.length;
if (len > (this.MaxEventListNum ? this.MaxEventListNum : this.defaultMaxEventListNum)) {
// 當超過最大限制,則會發除警告
ctx.maxed = true;
console.warn('events.MaxEventListNum || [ MaxEventListNum ] :The number of subscriptions exceeds the maximum, and if you do not set it, the default value is 10');
} else {
ctx.maxed = false;
}
}
}
...
現在Vue可謂是紅的發紫,沒關系,events-manage也可以在Vue中掛在到全局使用哦
events.prototype.install = function (Vue, Option) {
Vue.prototype.$ev = this;
}
不用多解釋了吧,想必看官都明白應該怎么使用了吧(在Vue中)
關於本庫更具體更詳細的使用文檔,趕緊戳這里
碼字不易啊,如果覺得對您有一些幫助,還請給一個大大的贊👍哈哈
(...已是凌晨...)
