jQuery技巧之讓任何組件都支持類似DOM的事件管理
本文介紹一個(gè)jquery的小技巧,能讓任意組件對(duì)象都能支持類似DOM的事件管理,也就是說(shuō)除了派發(fā)事件,添加或刪除事件監(jiān)聽(tīng)器,還能支持事件冒泡,阻止事件默認(rèn)行為等等。在jquery的幫助下,使用這個(gè)方法來(lái)管理普通對(duì)象的事件就跟管理DOM對(duì)象的事件一模一樣,雖然在最后當(dāng)你看到這個(gè)小技巧的具體內(nèi)容時(shí),你可能會(huì)覺(jué)得原來(lái)如此或者不過(guò)如此,但是我覺(jué)得如果能把普通的發(fā)布-訂閱模式的實(shí)現(xiàn)改成DOM類似的事件機(jī)制,那開(kāi)發(fā)出來(lái)的組件一定會(huì)有更大的靈活性和擴(kuò)展性,而且我也是第一次使用這種方法(見(jiàn)識(shí)太淺的原因),覺(jué)得它的使用價(jià)值還蠻大的,所以就把它分享出來(lái)了。
在正式介紹這個(gè)技巧之前,得先說(shuō)一下我之前考慮的一種方法,也就是發(fā)布-訂閱模式,看看它能解決什么問(wèn)題以及它存在的問(wèn)題。
1. 發(fā)布-訂閱模式
很多博客包括書(shū)本上都說(shuō)javascript要實(shí)現(xiàn)組件的自定義事件的話,可以采用發(fā)布-訂閱模式,起初我也是堅(jiān)定不移地這么認(rèn)為的,于是用jquery的$.Callbacks寫了一個(gè):
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('./class'); function isFunc(f) { return Object.prototype.toString.apply(f) === '[object Function]'; } /** * 這個(gè)基類可以讓普通的類具備事件驅(qū)動(dòng)的能力 * 提供類似jq的on off trigger方法,不考慮one方法,也不考慮命名空間 * 舉例: * var e = new EventBase(); * e.on('load', function(){ * console.log('loaded'); * }); * e.trigger('load');//loaded * e.off('load'); */ var EventBase = Class({ instanceMembers: { init: function () { this.events = {}; //把$.Callbacks的flag設(shè)置成一個(gè)實(shí)例屬性,以便子類可以覆蓋 this.CALLBACKS_FLAG = 'unique'; }, on: function (type, callback) { type = $.trim(type); //如果type或者callback參數(shù)無(wú)效則不處理 if (!(type && isFunc(callback))) return; var event = this.events[type]; if (!event) { //定義一個(gè)新的jq隊(duì)列,且該隊(duì)列不能添加重復(fù)的回調(diào) event = this.events[type] = $.Callbacks(this.CALLBACKS_FLAG); } //把callback添加到這個(gè)隊(duì)列中,這個(gè)隊(duì)列可以通過(guò)type來(lái)訪問(wèn) event.add(callback); }, off: function (type, callback) { type = $.trim(type); if (!type) return; var event = this.events[type]; if (!event) return; if (isFunc(callback)) { //如果同時(shí)傳遞type跟callback,則將callback從type對(duì)應(yīng)的隊(duì)列中移除 event.remove(callback); } else { //否則就移除整個(gè)type對(duì)應(yīng)的隊(duì)列 delete this.events[type]; } }, trigger: function () { var args = [].slice.apply(arguments), type = args[0];//第一個(gè)參數(shù)轉(zhuǎn)為type type = $.trim(type); if (!type) return; var event = this.events[type]; if (!event) return; //用剩下的參數(shù)來(lái)觸發(fā)type對(duì)應(yīng)的回調(diào) //同時(shí)把回調(diào)的上下文設(shè)置成當(dāng)前實(shí)例 event.fireWith(this, args.slice(1)); } } }); return EventBase; });
(基于seajs以及《詳解Javascript的繼承實(shí)現(xiàn)》介紹的繼承庫(kù)class.js)
只要任何組件繼承這個(gè)EventBase,就能繼承它提供的on off trigger方法來(lái)完成消息的訂閱,發(fā)布和取消訂閱功能,比如我下面想要實(shí)現(xiàn)的這個(gè)FileUploadBaseView:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('./class'); var EventBase = require('./eventBase'); var DEFAULTS = { data: [], //要展示的數(shù)據(jù)列表,列表元素必須是object類型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}] sizeLimit: 0, //用來(lái)限制BaseView中的展示的元素個(gè)數(shù),為0表示不限制 readonly: false, //用來(lái)控制BaseView中的元素是否允許增加和刪除 onBeforeRender: $.noop, //對(duì)應(yīng)beforeRender事件,在render方法調(diào)用前觸發(fā) onRender: $.noop, //對(duì)應(yīng)render事件,在render方法調(diào)用后觸發(fā) onBeforeAppend: $.noop, //對(duì)應(yīng)beforeAppend事件,在append方法調(diào)用前觸發(fā) onAppend: $.noop, //對(duì)應(yīng)append事件,在append方法調(diào)用后觸發(fā) onBeforeRemove: $.noop, //對(duì)應(yīng)beforeRemove事件,在remove方法調(diào)用前觸發(fā) onRemove: $.noop //對(duì)應(yīng)remove事件,在remove方法調(diào)用后觸發(fā) }; /** * 數(shù)據(jù)解析,給每個(gè)元素的添加一個(gè)唯一標(biāo)識(shí)_uuid,方便查找 */ function resolveData(ctx, data){ var time = new Date().getTime(); return $.map(data, function(d){ d._uuid = '_uuid' + time + Math.floor(Math.random() * 100000); }); } var FileUploadBaseView = Class({ instanceMembers: { init: function (options) { this.base(); this.options = this.getOptions(options); }, getOptions: function(options) { return $.extend({}, DEFAULTS, options); }, render: function(){ }, append: function(data){ }, remove: function(prop){ } }, extend: EventBase }); return FileUploadBaseView; });
實(shí)際調(diào)用測(cè)試如下:
測(cè)試中,實(shí)例化了一個(gè)FileUploadBaseView對(duì)象f,并設(shè)置了它的name屬性,通過(guò)on方法添加一個(gè)跟hello相關(guān)的監(jiān)聽(tīng)器,最后通過(guò)trigger方法觸發(fā)了hello的監(jiān)聽(tīng)器,并傳遞了額外的兩個(gè)參數(shù),在監(jiān)聽(tīng)器內(nèi)部除了可以通過(guò)監(jiān)聽(tīng)器的函數(shù)參數(shù)訪問(wèn)到trigger傳遞過(guò)來(lái)的數(shù)據(jù),還能通過(guò)this訪問(wèn)f對(duì)象。
從目前的結(jié)果來(lái)說(shuō),這個(gè)方式看起來(lái)還不錯(cuò),但是在我想要繼續(xù)實(shí)現(xiàn)FileUploadBaseView的時(shí)候碰到了問(wèn)題。你看我在設(shè)計(jì)這個(gè)組件的時(shí)候那幾個(gè)訂閱相關(guān)的option:
我原本的設(shè)計(jì)是:這些訂閱都是成對(duì)定義,一對(duì)訂閱跟某個(gè)實(shí)例方法對(duì)應(yīng),比如帶before的那個(gè)訂閱會(huì)在相應(yīng)的實(shí)例方法(render)調(diào)用前觸發(fā),不帶before的那個(gè)訂閱會(huì)在相應(yīng)的實(shí)例方法(render)調(diào)用后觸發(fā),而且還要求帶before的那個(gè)訂閱如果返回false,就不執(zhí)行相應(yīng)的實(shí)例方法以及后面的訂閱。最后這個(gè)設(shè)計(jì)要求是考慮到在調(diào)用組件的實(shí)例方法之前,有可能因?yàn)橐恍┨厥獾脑?,必須得取消?dāng)前實(shí)例方法的調(diào)用,比如調(diào)用remove方法時(shí)有的數(shù)據(jù)不能remove,那么就可以在before訂閱里面做一些校驗(yàn),能刪除的返回true,不能刪除的返回false,然后在實(shí)例方法中觸發(fā)before的訂閱后加一個(gè)判斷就可以了,類似下面的這種做法:
但是這個(gè)做法只能在單純的回調(diào)函數(shù)模式里實(shí)現(xiàn),在發(fā)布-訂閱模式下是行不通的,因?yàn)榛卣{(diào)函數(shù)只會(huì)跟一個(gè)函數(shù)引用相關(guān),而發(fā)布-訂閱模式里,同一個(gè)消息可能有多個(gè)訂閱,如果把這種做法應(yīng)用到發(fā)布-訂閱里面,當(dāng)調(diào)用this.trigger('beforeRender')的時(shí)候,會(huì)把跟beforeRender關(guān)聯(lián)的所有訂閱全部調(diào)用一次,那么以哪個(gè)訂閱的返回值為準(zhǔn)呢?也許你會(huì)說(shuō)可以用隊(duì)列中的最后一個(gè)訂閱的返回值為準(zhǔn),在大多數(shù)情況下也許這么干沒(méi)問(wèn)題,但是當(dāng)我們把“以隊(duì)列最后的一個(gè)訂閱返回值作為判斷標(biāo)準(zhǔn)”這個(gè)邏輯加入到EventBase中的時(shí)候,會(huì)出現(xiàn)一個(gè)很大的風(fēng)險(xiǎn),就是外部在使用的時(shí)候,一定得清楚地管理好訂閱的順序,一定要把那個(gè)跟校驗(yàn)等一些特殊邏輯相關(guān)的訂閱放在最后面才行,而這種跟語(yǔ)法、編譯沒(méi)有關(guān)系,對(duì)編碼順序有要求的開(kāi)發(fā)方式會(huì)給軟件帶來(lái)比較大的安全隱患,誰(shuí)能保證任何時(shí)候任何場(chǎng)景都能控制好訂閱的順序呢,更何況公司里面可能還有些后來(lái)的新人,壓根不知道你寫的東西還有這樣的限制。
解決這個(gè)問(wèn)題的完美方式,就是像DOM對(duì)象的事件那樣,在消息發(fā)布的時(shí)候,不是簡(jiǎn)簡(jiǎn)單單的發(fā)布一個(gè)消息字符串,而是把這個(gè)消息封裝成一個(gè)對(duì)象,這個(gè)對(duì)象會(huì)傳遞給它所有的訂閱,哪個(gè)訂閱里覺(jué)得應(yīng)該阻止這個(gè)消息發(fā)布之后的邏輯,只要調(diào)用這個(gè)消息的preventDefault()方法,然后在外部發(fā)布完消息后,調(diào)用消息的isDefaultPrevented()方法判斷一下即可:
而這個(gè)做法跟使用jquery管理DOM對(duì)象的事件是一樣的思路,比如bootstrap的大部分組件以及我在前面一些博客中寫的組件都是用的這個(gè)方法來(lái)增加額外的判斷邏輯,比如bootstrap的alert組件在close方法執(zhí)行的時(shí)候有一段這樣的判斷:
按照這個(gè)思路去改造EventBase是一個(gè)解決問(wèn)題的方法,但是jquery的一個(gè)小技巧,能夠讓我們把整個(gè)普通對(duì)象的事件管理變得更加簡(jiǎn)單,下面就讓我們來(lái)瞧一瞧它的廬山真面目。
2. jquery小技巧模式
1)技巧一
如果在定義組件的時(shí)候,這個(gè)組件是跟DOM對(duì)象有關(guān)聯(lián)的,比如下面這種形式:
那么我們可以完全給這個(gè)組件添加on off trigger one這幾個(gè)常用事件管理的方法,然后將這些方法代理到$element的相應(yīng)方法上:
通過(guò)代理,當(dāng)調(diào)用組件的on方法時(shí),其實(shí)調(diào)用的是$element的on方法,這樣的話這種類型的組件就能支持完美的事件管理了。
2)技巧二
第一個(gè)技巧只能適用于跟DOM有關(guān)聯(lián)的組件,對(duì)于那些跟DOM完全沒(méi)有關(guān)聯(lián)的組件該怎么添加像前面這樣完美的事件管理機(jī)制呢?其實(shí)方法也很簡(jiǎn)單,只是我自己以前真的是沒(méi)這么用過(guò),所以這一次用起來(lái)才會(huì)覺(jué)得特別新鮮:
看截圖中框起來(lái)的部分,只要給jquery的構(gòu)造函數(shù)傳遞一個(gè)空對(duì)象,它就會(huì)返回一個(gè)完美支持事件管理的jquery對(duì)象。而且除了事件管理的功能外,由于它是一個(gè)jquery對(duì)象。所以jquery原型上的所有方法它都能調(diào)用,將來(lái)要是需要借用jquery其它的跟DOM無(wú)關(guān)的方法,說(shuō)不定也能參考這個(gè)小技巧來(lái)實(shí)現(xiàn)。
3. 完美的事件管理實(shí)現(xiàn)
考慮到第2部分介紹的2種方式里面有重復(fù)的邏輯代碼,如果把它們結(jié)合起來(lái)的話,就可以適用所有的開(kāi)發(fā)組件的場(chǎng)景,也就能達(dá)到本文標(biāo)題和開(kāi)篇提到的讓任意對(duì)象支持事件管理功能的目標(biāo)了,所以最后結(jié)合前面兩個(gè)技巧,把EventBase改造如下(是不是夠簡(jiǎn)單):
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('./class'); /** * 這個(gè)基類可以讓普通的類具備jquery對(duì)象的事件管理能力 */ var EventBase = Class({ instanceMembers: { init: function (_jqObject) { this._jqObject = _jqObject && _jqObject instanceof $ && _jqObject || $({}); }, on: function(){ return $.fn.on.apply(this._jqObject, arguments); }, one: function(){ return $.fn.one.apply(this._jqObject, arguments); }, off: function(){ return $.fn.off.apply(this._jqObject, arguments); }, trigger: function(){ return $.fn.trigger.apply(this._jqObject, arguments); } } }); return EventBase; });
實(shí)際調(diào)用測(cè)試如下
1)模擬跟DOM關(guān)聯(lián)的組件
測(cè)試代碼一:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var Demo = window.demo = Class({ instanceMembers: { init: function (element,options) { this.$element = $(element); this.base(this.$element); //添加監(jiān)聽(tīng) this.on('beforeRender', $.proxy(options.onBeforeRender, this)); this.on('render', $.proxy(options.onRender, this)); }, render: function () { //觸發(fā)beforeRender事件 var e = $.Event('beforeRender'); this.trigger(e); if(e.isDefaultPrevented())return; //主要邏輯代碼 console.log('render complete!'); //觸發(fā)render事件 this.trigger('render'); } }, extend: EventBase }); var demo = new Demo('#demo', { onBeforeRender: function(e) { console.log('beforeRender event triggered!'); }, onRender: function(e) { console.log('render event triggered!'); } }); demo.render(); });
在這個(gè)測(cè)試?yán)铮?我定義了一個(gè)跟DOM關(guān)聯(lián)的Demo組件并繼承了EventBase這個(gè)事件管理的類,給beforeRender事件和render事件都添加了一個(gè)監(jiān)聽(tīng),render方法中也有打印信息來(lái)模擬真實(shí)的邏輯,實(shí)例化Demo的時(shí)候用到了#demo這個(gè)DOM元素,最后的測(cè)試結(jié)果是:
完全與預(yù)期一致。
測(cè)試代碼二:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var Demo = window.demo = Class({ instanceMembers: { init: function (element,options) { this.$element = $(element); this.base(this.$element); //添加監(jiān)聽(tīng) this.on('beforeRender', $.proxy(options.onBeforeRender, this)); this.on('render', $.proxy(options.onRender, this)); }, render: function () { //觸發(fā)beforeRender事件 var e = $.Event('beforeRender'); this.trigger(e); if(e.isDefaultPrevented())return; //主要邏輯代碼 console.log('render complete!'); //觸發(fā)render事件 this.trigger('render'); } }, extend: EventBase }); var demo = new Demo('#demo', { onBeforeRender: function(e) { console.log('beforeRender event triggered!'); }, onRender: function(e) { console.log('render event triggered!'); } }); demo.on('beforeRender', function(e) { e.preventDefault(); console.log('beforeRender event triggered 2!'); }); demo.on('beforeRender', function(e) { console.log('beforeRender event triggered 3!'); }); demo.render(); });
在這個(gè)測(cè)試了, 我定義了一個(gè)跟DOM相關(guān)的Demo組件并繼承了EventBase這個(gè)事件管理的類,給beforeRender事件添加了3個(gè)監(jiān)聽(tīng),其中一個(gè)有加prevetDefault()的調(diào)用,而且該回調(diào)還不是最后一個(gè),最后的測(cè)試結(jié)果是:
從結(jié)果可以看到,render方法的主要邏輯代碼跟后面的render事件都沒(méi)有執(zhí)行,所有beforeRender的監(jiān)聽(tīng)器都執(zhí)行了,說(shuō)明e.preventDefault()生效了,而且它沒(méi)有對(duì)beforeRender的事件隊(duì)列產(chǎn)生影響。
2)模擬跟DOM無(wú)關(guān)聯(lián)的普通對(duì)象
測(cè)試代碼一:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var Demo = window.demo = Class({ instanceMembers: { init: function (options) { this.base(); //添加監(jiān)聽(tīng) this.on('beforeRender', $.proxy(options.onBeforeRender, this)); this.on('render', $.proxy(options.onRender, this)); }, render: function () { //觸發(fā)beforeRender事件 var e = $.Event('beforeRender'); this.trigger(e); if(e.isDefaultPrevented())return; //主要邏輯代碼 console.log('render complete!'); //觸發(fā)render事件 this.trigger('render'); } }, extend: EventBase }); var demo = new Demo({ onBeforeRender: function(e) { console.log('beforeRender event triggered!'); }, onRender: function(e) { console.log('render event triggered!'); } }); demo.render(); });
在這個(gè)測(cè)試?yán)铮?我定義了一個(gè)跟DOM無(wú)關(guān)的Demo組件并繼承了EventBase這個(gè)事件管理的類,給beforeRender事件和render事件都添加了一個(gè)監(jiān)聽(tīng),render方法中也有打印信息來(lái)模擬真實(shí)的邏輯,最后的測(cè)試結(jié)果是:
完全與預(yù)期的一致。
測(cè)試代碼二:
define(function(require, exports, module) { var $ = require('jquery'); var Class = require('mod/class'); var EventBase = require('mod/eventBase'); var Demo = window.demo = Class({ instanceMembers: { init: function (options) { this.base(); //添加監(jiān)聽(tīng) this.on('beforeRender', $.proxy(options.onBeforeRender, this)); this.on('render', $.proxy(options.onRender, this)); }, render: function () { //觸發(fā)beforeRender事件 var e = $.Event('beforeRender'); this.trigger(e); if(e.isDefaultPrevented())return; //主要邏輯代碼 console.log('render complete!'); //觸發(fā)render事件 this.trigger('render'); } }, extend: EventBase }); var demo = new Demo({ onBeforeRender: function(e) { console.log('beforeRender event triggered!'); }, onRender: function(e) { console.log('render event triggered!'); } }); demo.on('beforeRender', function(e) { e.preventDefault(); console.log('beforeRender event triggered 2!'); }); demo.on('beforeRender', function(e) { console.log('beforeRender event triggered 3!'); }); demo.render(); });
在這個(gè)測(cè)試了, 我定義了一個(gè)跟DOM無(wú)關(guān)的Demo組件并繼承了EventBase這個(gè)事件管理的類,給beforeRender事件添加了3個(gè)監(jiān)聽(tīng),其中一個(gè)有加prevetDefault()的調(diào)用,而且該回調(diào)還不是最后一個(gè),最后的測(cè)試結(jié)果是:
從結(jié)果可以看到,render方法的主要邏輯代碼跟后面的render事件都沒(méi)有執(zhí)行,所有beforeRender的監(jiān)聽(tīng)器都執(zhí)行了,說(shuō)明e.preventDefault()生效了,而且它沒(méi)有對(duì)beforeRender的事件隊(duì)列產(chǎn)生影響。
所以從2個(gè)測(cè)試來(lái)看,通過(guò)改造后的EventBase,我們得到了一個(gè)可以讓任意對(duì)象支持jquery事件管理機(jī)制的方法,將來(lái)在考慮用事件機(jī)制來(lái)解耦的時(shí)候,就不用再去考慮前面第一個(gè)介紹的發(fā)布-訂閱模式了,而且相對(duì)而言這個(gè)方法功能更強(qiáng)更穩(wěn)定,也更符合你平常使用jquery操作DOM的習(xí)慣。
4. 本文小結(jié)
有2點(diǎn)需要再說(shuō)明一下的是:
1)即使不用jquery按照第1部分最后提出的思路,把第一部分常規(guī)的發(fā)布-訂閱模式改造一下也可以的,只不過(guò)用jquery更加簡(jiǎn)潔些;
2)最終用jquery 的事件機(jī)制來(lái)實(shí)現(xiàn)任意對(duì)象的事件管理,一方面是用到了代理模式,更重要的還是要用發(fā)布-訂閱模式,只不過(guò)最后的這個(gè)實(shí)現(xiàn)是由jquery幫我們把第一部分的發(fā)布-訂閱實(shí)現(xiàn)改造好了而已。
以上內(nèi)容是針對(duì)jQuery技巧之讓任何組件都支持類似DOM的事件管理的相關(guān)知識(shí),希望對(duì)大家有所幫助!
相關(guān)文章
修改Jquery Dialog 位置的實(shí)現(xiàn)方法
下面小編就為大家?guī)?lái)一篇修改Jquery Dialog 位置的實(shí)現(xiàn)方法。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2016-08-08如何用jQuery實(shí)現(xiàn)ASP.NET GridView折疊伸展效果
我們今天就一個(gè)具體的需求進(jìn)行分析,引出如何用jQuery實(shí)現(xiàn)ASP.NET GridView折疊伸展效果,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2015-09-09jQuery模擬下拉框選擇對(duì)應(yīng)菜單的內(nèi)容
這篇文章主要介紹了jQuery模擬下拉框選擇對(duì)應(yīng)菜單的內(nèi)容,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下2017-03-03jQuery實(shí)現(xiàn)的支持IE的html滑動(dòng)條
本文給大家分享的是一段使用jQuery實(shí)現(xiàn)支持IE的html滑動(dòng)條代碼,效果非常不錯(cuò),這里推薦給大家,希望大家能夠喜歡。2015-03-03jquery.validate提示錯(cuò)誤信息位置方法
這篇文章主要介紹了jquery.validate提示錯(cuò)誤信息位置方法,實(shí)例分析了jquery.validate實(shí)現(xiàn)提示錯(cuò)誤信息位置的相關(guān)技巧,需要的朋友可以參考下2016-01-01jquery增加時(shí)編輯jqGrid(實(shí)例代碼)
jquery增加時(shí)編輯jqGrid(實(shí)例代碼)。需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-11-11Jquery+AJAX實(shí)現(xiàn)無(wú)刷新上傳并重命名文件操作示例【PHP后臺(tái)接收】
這篇文章主要介紹了Jquery+AJAX實(shí)現(xiàn)無(wú)刷新上傳并重命名文件操作,結(jié)合實(shí)例形式分析了jQuery+ajax前臺(tái)上傳文件與PHP后臺(tái)接收處理相關(guān)操作技巧,需要的朋友可以參考下2020-05-05jQuery彈簧插件編寫基礎(chǔ)之“又見(jiàn)彈窗”
本文通過(guò)具體實(shí)例給大家介紹jquery彈窗插件編寫基礎(chǔ)之又見(jiàn)彈簧的相關(guān)資料,對(duì)jquery彈簧插件編寫相關(guān)知識(shí)感興趣的朋友一起學(xué)習(xí)吧2015-12-12