JavaScript: 實現自定義事件
無論是從事web開發還是從事GUI開發,事件都是我們經常使用到的。事件又被稱為觀察者模式或訂閱/發布,拿HTML來說,一個DIV可以觸發click事件,這個事件類型click是對外公開的,所以我們可以去訂閱它。如果通過DIV去訂閱一個未知的事件類型,則其結果是未定義的。所以事件click在接受對外訂閱之前,需要對外發布。當鼠標在DIV上點擊時,click事件就被觸發。
jQuery的事件機制
普通對象通過jQuery包裝后即擁有自定義事件功能(當然擁有的功能非常多,但這里只關注自定義事件),并且jQuery的自定義事件被實現為無須對外發布事件即可被訂閱。來看個例子:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test Event</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="Test Event" /> <meta name="description" content="Test Event" /> <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script> </head> <body> <script type="text/javascript"> var EventObject = jQuery({}); EventObject.bind('GO_TO_BED', function(event, name, hour) { console.group("Test Event"); console.log("event object: ", event); console.log("name: ", name); console.log("hour: ", hour); }); EventObject.trigger('GO_TO_BED', ['goal', 12]); </script> </body> </html>
先bind,后trigger,這是有原因的,下文將詳細解釋這點。事件類型為GO_TO_BED,使用大寫的事件類型是一個約定,我們不妨遵循這條規則好了。執行結果如下圖所示:
在trigger時所傳的參數被完整的傳到bind時指定的事件句柄中,至于傳參的方式,這只是實現上的細節。上述代碼的bind是用于訂閱事件,trigger用于觸發事件。bind和trigger的第一個參數都是事件類型并且都是同一個事件類型才能被觸發。而bind方法的第二個參數為GO_TO_BED事件被觸發時所執行的函數。
實現自定義事件的思路
什么是發布事件
發布事件其實是指定可用的事件類型列表。當然這個并非一定要實現,類似jQuery方式的也是可行的。
什么是事件類型
事件類型其實是相當于一個查找key,而這個key可以關聯多個函數。所以這個事件類型應該是Map的一個key,這個key被關聯到一個待執行函數列表。我們暫且將這個Map定義為eventsList。
什么是事件訂閱
事件訂閱是往eventsList里添加事件類型key和它所關聯的待執行函數。當然如果eventsList里已經存在某個key,則僅僅是將待執行函數添加到隊列尾。
什么是事件觸發
事件觸發令所指定的事件類型key所關聯的待執行函數列表有機會逐一執行。
事件機制的簡單實現
為了對自定義事件機制有個大概的印象,下面簡單實現了一個,只包括發布事件、訂閱事件和觸發事件功能。而且在訂閱事件和觸發事件時并沒有去檢測有沒有公開相應的事件類型。代碼如下:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test Event</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="Test Event" /> <meta name="description" content="Test Event" /> <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script> </head> <body> <input type="button" value="Test Event" /> <script type="text/javascript"> // 事件類 function Observer() { this._eventsList = {}; // {'eat' : [{fn : null, scope : null}, {fn : null, scope : null}]} } Observer.prototype = { dispatchEvent : function(eName) { eName = eName.toLowerCase(); this._eventsList[eName] = []; }, on : function(eName, fn, scope) { eName = eName.toLowerCase(); this._eventsList[eName].push({fn : fn || null, scope : scope || null}); }, fireEvent : function() { var args = Array.prototype.slice.call(arguments); var eName = args.shift(); eName = eName.toLowerCase(); var list = this._eventsList[eName]; for (var i = 0; i < list.length; i++) { var dict = list[i]; var fn = dict.fn; var scope = dict.scope; fn.apply(scope || null, args); } } }; // end var EventObject = new Observer(); EventObject.dispatchEvent('GO_TO_BED'); EventObject.on('GO_TO_BED', function(name, hour) { console.group('Test Event'); console.log(name + '要在' + hour + '點之前去睡覺'); }); ~function($) { $(function() { $("input").click(function(event) { event.stopPropagation(); EventObject.fireEvent('GO_TO_BED', 'goal', 12); }); }); }(jQuery) </script> </body> </html>
執行結果如下:
事件機制的完整實現
為什么要先訂閱再觸發呢?因為訂閱是往eventsList添加key和可執行函數列表,如果顛倒了順序,則在觸發事件時eventsList中事件類型key所關聯的可執行函數列表是空的,也就沒什么可執行的了。下面是一個比較完整的實現:
<!DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Test Event</title> <meta name="author" content="" /> <meta http-equiv="X-UA-Compatible" content="IE=7" /> <meta name="keywords" content="Test Event" /> <meta name="description" content="Test Event" /> <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script> </head> <body> <input type="button" value="Test Event" /> <script type="text/javascript"> /** * 觀察者模式實現事件監聽 */ function Observer() { this._eventsList = {}; // 對外發布的事件列表{"connect" : [{fn : null, scope : null}, {fn : null, scope : null}]} } Observer.prototype = { // 空函數 _emptyFn : function() { }, /** * 判斷事件是否已發布 * @param eType 事件類型 * @return Boolean */ _hasDispatch : function(eType) { eType = (String(eType) || '').toLowerCase(); return "undefined" !== typeof this._eventsList[eType]; }, /** * 根據事件類型查對fn所在的索引,如果不存在將返回-1 * @param eType 事件類型 * @param fn 事件句柄 */ _indexFn : function(eType, fn) { if(!this._hasDispatch(eType)) { return -1; } var list = this._eventsList[eType]; fn = fn || ''; for(var i = 0; i < list.length; i++) { var dict = list[i]; var _fn = dict.fn || ''; if(fn.toString() === _fn.toString()) { return i; } } return -1; }, /** * 創建委托 */ createDelegate : function() { var __method = this; var args = Array.prototype.slice.call(arguments); var object = args.shift(); return function() { return __method.apply(object, args.concat(Array.prototype.slice.call(arguments))); } }, /** * 發布事件 */ dispatchEvent : function() { if(arguments.length < 1) { return false; } var args = Array.prototype.slice.call(arguments), _this = this; $.each(args, function(index, eType){ if(_this._hasDispatch(eType)) { return true; } _this._eventsList[eType.toLowerCase()] = []; }); return this; }, /** * 觸發事件 */ fireEvent : function() { if(arguments.length < 1) { return false; } var args = Array.prototype.slice.call(arguments), eType = args.shift().toLowerCase(), _this = this; if(this._hasDispatch(eType)) { var list = this._eventsList[eType]; if (!list) { return this; } $.each(list, function(index, dict){ var fn = dict.fn, scope = dict.scope || _this; if(!fn || "function" !== typeof fn) { fn = _this._emptyFn; } if(true === scope) { scope = null; } fn.apply(scope, args); }); } return this; }, /** * 訂閱事件 * @param eType 事件類型 * @param fn 事件句柄 * @param scope */ on : function(eType, fn, scope) { eType = (eType || '').toLowerCase(); if(!this._hasDispatch(eType)) { throw new Error("not dispatch event " + eType); return false; } this._eventsList[eType].push({fn : fn || null, scope : scope || null}); return this; }, /** * 取消訂閱某個事件 * @param eType 事件類型 * @param fn 事件句柄 */ un : function(eType, fn) { eType = (eType || '').toLowerCase(); if(this._hasDispatch(eType)) { var index = this._indexFn(eType, fn); if(index > -1) { var list = this._eventsList[eType]; list.splice(index, 1); } } return this; }, /** * 取消訂閱所有事件 */ die : function(eType) { eType = (eType || '').toLowerCase(); if(this._eventsList[eType]) { this._eventsList[eType] = []; } return this; } }; // end var EventObject = new Observer(); EventObject.dispatchEvent('GO_TO_BED'); EventObject.on('GO_TO_BED', function(name, hour) { console.group('Test Event'); console.log(name + '要在' + hour + '點之前去睡覺,誰又懂得了碼農的辛酸啊?'); }); ~function($) { $(function() { $("input").click(function(event) { event.stopPropagation(); EventObject.fireEvent('GO_TO_BED', 'goal', 12); }); }); }(jQuery) </script> </body> </html>
以上代碼完整的實現了發布事件、訂閱事件、觸發事件以及取消訂閱功能。執行結果如下:
結束語
在有需要的時候可以將EventObject組合到其它類中來使用,或者模擬類的實現和繼承,為代碼解耦發力。