亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

jQuery技巧之讓任何組件都支持類似DOM的事件管理

2019-11-20 10:17:46
字體:
來源:轉載
供稿:網友

本文介紹一個jquery的小技巧,能讓任意組件對象都能支持類似DOM的事件管理,也就是說除了派發事件,添加或刪除事件監聽器,還能支持事件冒泡,阻止事件默認行為等等。在jquery的幫助下,使用這個方法來管理普通對象的事件就跟管理DOM對象的事件一模一樣,雖然在最后當你看到這個小技巧的具體內容時,你可能會覺得原來如此或者不過如此,但是我覺得如果能把普通的發布-訂閱模式的實現改成DOM類似的事件機制,那開發出來的組件一定會有更大的靈活性和擴展性,而且我也是第一次使用這種方法(見識太淺的原因),覺得它的使用價值還蠻大的,所以就把它分享出來了。

在正式介紹這個技巧之前,得先說一下我之前考慮的一種方法,也就是發布-訂閱模式,看看它能解決什么問題以及它存在的問題。

1. 發布-訂閱模式

很多博客包括書本上都說javascript要實現組件的自定義事件的話,可以采用發布-訂閱模式,起初我也是堅定不移地這么認為的,于是用jquery的$.Callbacks寫了一個:

define(function(require, exports, module) {var $ = require('jquery');var Class = require('./class');function isFunc(f) {return Object.prototype.toString.apply(f) === '[object Function]';}/*** 這個基類可以讓普通的類具備事件驅動的能力* 提供類似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設置成一個實例屬性,以便子類可以覆蓋this.CALLBACKS_FLAG = 'unique';},on: function (type, callback) {type = $.trim(type);//如果type或者callback參數無效則不處理if (!(type && isFunc(callback))) return;var event = this.events[type];if (!event) {//定義一個新的jq隊列,且該隊列不能添加重復的回調event = this.events[type] = $.Callbacks(this.CALLBACKS_FLAG);}//把callback添加到這個隊列中,這個隊列可以通過type來訪問event.add(callback);},off: function (type, callback) {type = $.trim(type);if (!type) return;var event = this.events[type];if (!event) return;if (isFunc(callback)) {//如果同時傳遞type跟callback,則將callback從type對應的隊列中移除event.remove(callback);} else {//否則就移除整個type對應的隊列delete this.events[type];}},trigger: function () {var args = [].slice.apply(arguments),type = args[0];//第一個參數轉為typetype = $.trim(type);if (!type) return;var event = this.events[type];if (!event) return;//用剩下的參數來觸發type對應的回調//同時把回調的上下文設置成當前實例event.fireWith(this, args.slice(1));}}});return EventBase;});

(基于seajs以及《詳解Javascript的繼承實現》介紹的繼承庫class.js)

只要任何組件繼承這個EventBase,就能繼承它提供的on off trigger方法來完成消息的訂閱,發布和取消訂閱功能,比如我下面想要實現的這個FileUploadBaseView:

define(function(require, exports, module) {var $ = require('jquery');var Class = require('./class');var EventBase = require('./eventBase');var DEFAULTS = {data: [], //要展示的數據列表,列表元素必須是object類型的,如[{url: 'xxx.png'},{url: 'yyyy.png'}]sizeLimit: 0, //用來限制BaseView中的展示的元素個數,為0表示不限制readonly: false, //用來控制BaseView中的元素是否允許增加和刪除onBeforeRender: $.noop, //對應beforeRender事件,在render方法調用前觸發onRender: $.noop, //對應render事件,在render方法調用后觸發onBeforeAppend: $.noop, //對應beforeAppend事件,在append方法調用前觸發onAppend: $.noop, //對應append事件,在append方法調用后觸發onBeforeRemove: $.noop, //對應beforeRemove事件,在remove方法調用前觸發onRemove: $.noop //對應remove事件,在remove方法調用后觸發};/*** 數據解析,給每個元素的添加一個唯一標識_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;});

實際調用測試如下:



測試中,實例化了一個FileUploadBaseView對象f,并設置了它的name屬性,通過on方法添加一個跟hello相關的監聽器,最后通過trigger方法觸發了hello的監聽器,并傳遞了額外的兩個參數,在監聽器內部除了可以通過監聽器的函數參數訪問到trigger傳遞過來的數據,還能通過this訪問f對象。

從目前的結果來說,這個方式看起來還不錯,但是在我想要繼續實現FileUploadBaseView的時候碰到了問題。你看我在設計這個組件的時候那幾個訂閱相關的option:

 

我原本的設計是:這些訂閱都是成對定義,一對訂閱跟某個實例方法對應,比如帶before的那個訂閱會在相應的實例方法(render)調用前觸發,不帶before的那個訂閱會在相應的實例方法(render)調用后觸發,而且還要求帶before的那個訂閱如果返回false,就不執行相應的實例方法以及后面的訂閱。最后這個設計要求是考慮到在調用組件的實例方法之前,有可能因為一些特殊的原因,必須得取消當前實例方法的調用,比如調用remove方法時有的數據不能remove,那么就可以在before訂閱里面做一些校驗,能刪除的返回true,不能刪除的返回false,然后在實例方法中觸發before的訂閱后加一個判斷就可以了,類似下面的這種做法:

但是這個做法只能在單純的回調函數模式里實現,在發布-訂閱模式下是行不通的,因為回調函數只會跟一個函數引用相關,而發布-訂閱模式里,同一個消息可能有多個訂閱,如果把這種做法應用到發布-訂閱里面,當調用this.trigger('beforeRender')的時候,會把跟beforeRender關聯的所有訂閱全部調用一次,那么以哪個訂閱的返回值為準呢?也許你會說可以用隊列中的最后一個訂閱的返回值為準,在大多數情況下也許這么干沒問題,但是當我們把“以隊列最后的一個訂閱返回值作為判斷標準”這個邏輯加入到EventBase中的時候,會出現一個很大的風險,就是外部在使用的時候,一定得清楚地管理好訂閱的順序,一定要把那個跟校驗等一些特殊邏輯相關的訂閱放在最后面才行,而這種跟語法、編譯沒有關系,對編碼順序有要求的開發方式會給軟件帶來比較大的安全隱患,誰能保證任何時候任何場景都能控制好訂閱的順序呢,更何況公司里面可能還有些后來的新人,壓根不知道你寫的東西還有這樣的限制。

解決這個問題的完美方式,就是像DOM對象的事件那樣,在消息發布的時候,不是簡簡單單的發布一個消息字符串,而是把這個消息封裝成一個對象,這個對象會傳遞給它所有的訂閱,哪個訂閱里覺得應該阻止這個消息發布之后的邏輯,只要調用這個消息的preventDefault()方法,然后在外部發布完消息后,調用消息的isDefaultPrevented()方法判斷一下即可:

而這個做法跟使用jquery管理DOM對象的事件是一樣的思路,比如bootstrap的大部分組件以及我在前面一些博客中寫的組件都是用的這個方法來增加額外的判斷邏輯,比如bootstrap的alert組件在close方法執行的時候有一段這樣的判斷:

按照這個思路去改造EventBase是一個解決問題的方法,但是jquery的一個小技巧,能夠讓我們把整個普通對象的事件管理變得更加簡單,下面就讓我們來瞧一瞧它的廬山真面目。

2. jquery小技巧模式

1)技巧一

如果在定義組件的時候,這個組件是跟DOM對象有關聯的,比如下面這種形式:

那么我們可以完全給這個組件添加on off trigger one這幾個常用事件管理的方法,然后將這些方法代理到$element的相應方法上:

通過代理,當調用組件的on方法時,其實調用的是$element的on方法,這樣的話這種類型的組件就能支持完美的事件管理了。

2)技巧二

第一個技巧只能適用于跟DOM有關聯的組件,對于那些跟DOM完全沒有關聯的組件該怎么添加像前面這樣完美的事件管理機制呢?其實方法也很簡單,只是我自己以前真的是沒這么用過,所以這一次用起來才會覺得特別新鮮:

看截圖中框起來的部分,只要給jquery的構造函數傳遞一個空對象,它就會返回一個完美支持事件管理的jquery對象。而且除了事件管理的功能外,由于它是一個jquery對象。所以jquery原型上的所有方法它都能調用,將來要是需要借用jquery其它的跟DOM無關的方法,說不定也能參考這個小技巧來實現。

3. 完美的事件管理實現

考慮到第2部分介紹的2種方式里面有重復的邏輯代碼,如果把它們結合起來的話,就可以適用所有的開發組件的場景,也就能達到本文標題和開篇提到的讓任意對象支持事件管理功能的目標了,所以最后結合前面兩個技巧,把EventBase改造如下(是不是夠簡單):

define(function(require, exports, module) {var $ = require('jquery');var Class = require('./class');/*** 這個基類可以讓普通的類具備jquery對象的事件管理能力*/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;});

實際調用測試如下

1)模擬跟DOM關聯的組件

測試代碼一:

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);//添加監聽this.on('beforeRender', $.proxy(options.onBeforeRender, this));this.on('render', $.proxy(options.onRender, this));},render: function () {//觸發beforeRender事件var e = $.Event('beforeRender');this.trigger(e);if(e.isDefaultPrevented())return;//主要邏輯代碼console.log('render complete!');//觸發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();});

在這個測試里, 我定義了一個跟DOM關聯的Demo組件并繼承了EventBase這個事件管理的類,給beforeRender事件和render事件都添加了一個監聽,render方法中也有打印信息來模擬真實的邏輯,實例化Demo的時候用到了#demo這個DOM元素,最后的測試結果是:

完全與預期一致。

測試代碼二:

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);//添加監聽this.on('beforeRender', $.proxy(options.onBeforeRender, this));this.on('render', $.proxy(options.onRender, this));},render: function () {//觸發beforeRender事件var e = $.Event('beforeRender');this.trigger(e);if(e.isDefaultPrevented())return;//主要邏輯代碼console.log('render complete!');//觸發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();});

在這個測試了, 我定義了一個跟DOM相關的Demo組件并繼承了EventBase這個事件管理的類,給beforeRender事件添加了3個監聽,其中一個有加prevetDefault()的調用,而且該回調還不是最后一個,最后的測試結果是:

從結果可以看到,render方法的主要邏輯代碼跟后面的render事件都沒有執行,所有beforeRender的監聽器都執行了,說明e.preventDefault()生效了,而且它沒有對beforeRender的事件隊列產生影響。

2)模擬跟DOM無關聯的普通對象

測試代碼一:

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();//添加監聽this.on('beforeRender', $.proxy(options.onBeforeRender, this));this.on('render', $.proxy(options.onRender, this));},render: function () {//觸發beforeRender事件var e = $.Event('beforeRender');this.trigger(e);if(e.isDefaultPrevented())return;//主要邏輯代碼console.log('render complete!');//觸發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();});

在這個測試里, 我定義了一個跟DOM無關的Demo組件并繼承了EventBase這個事件管理的類,給beforeRender事件和render事件都添加了一個監聽,render方法中也有打印信息來模擬真實的邏輯,最后的測試結果是:

完全與預期的一致。

測試代碼二:

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();//添加監聽this.on('beforeRender', $.proxy(options.onBeforeRender, this));this.on('render', $.proxy(options.onRender, this));},render: function () {//觸發beforeRender事件var e = $.Event('beforeRender');this.trigger(e);if(e.isDefaultPrevented())return;//主要邏輯代碼console.log('render complete!');//觸發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();});

在這個測試了, 我定義了一個跟DOM無關的Demo組件并繼承了EventBase這個事件管理的類,給beforeRender事件添加了3個監聽,其中一個有加prevetDefault()的調用,而且該回調還不是最后一個,最后的測試結果是:

從結果可以看到,render方法的主要邏輯代碼跟后面的render事件都沒有執行,所有beforeRender的監聽器都執行了,說明e.preventDefault()生效了,而且它沒有對beforeRender的事件隊列產生影響。

所以從2個測試來看,通過改造后的EventBase,我們得到了一個可以讓任意對象支持jquery事件管理機制的方法,將來在考慮用事件機制來解耦的時候,就不用再去考慮前面第一個介紹的發布-訂閱模式了,而且相對而言這個方法功能更強更穩定,也更符合你平常使用jquery操作DOM的習慣。

4. 本文小結

有2點需要再說明一下的是:

1)即使不用jquery按照第1部分最后提出的思路,把第一部分常規的發布-訂閱模式改造一下也可以的,只不過用jquery更加簡潔些;

2)最終用jquery 的事件機制來實現任意對象的事件管理,一方面是用到了代理模式,更重要的還是要用發布-訂閱模式,只不過最后的這個實現是由jquery幫我們把第一部分的發布-訂閱實現改造好了而已。

以上內容是針對jQuery技巧之讓任何組件都支持類似DOM的事件管理的相關知識,希望對大家有所幫助!

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
日韩欧美999| 欧美不卡视频一区发布| 日韩国产精品视频| 91麻豆国产语对白在线观看| 97国产在线观看| 国产精品亚洲一区二区三区| 久久在线观看视频| 亚洲激情电影中文字幕| 日韩国产精品一区| 亚洲精品视频中文字幕| 欧美日韩成人在线视频| 日韩精品中文字幕有码专区| 亚洲午夜久久久影院| 欧美激情一级欧美精品| 久久久91精品| 法国裸体一区二区| 欧美激情网站在线观看| 久久夜色精品国产亚洲aⅴ| xx视频.9999.com| 日本亚洲精品在线观看| 国产亚洲视频在线| 黄色成人av网| 午夜剧场成人观在线视频免费观看| 久久久免费观看视频| 日韩欧美在线观看视频| 亚洲www在线观看| 亚洲第一免费网站| 日韩精品中文字| 欧美午夜www高清视频| 中文字幕在线观看亚洲| 国产日韩换脸av一区在线观看| 国产在线精品成人一区二区三区| 日韩精品日韩在线观看| 91夜夜揉人人捏人人添红杏| 欧美日韩国产影院| 国产99久久精品一区二区 夜夜躁日日躁| 奇米成人av国产一区二区三区| 亚洲а∨天堂久久精品9966| 亚洲美女性视频| 97成人精品视频在线观看| 欧美激情三级免费| 成人国产亚洲精品a区天堂华泰| 国产精品久久久久久久久久99| 91国产美女在线观看| 精品国产91乱高清在线观看| 中文字幕在线看视频国产欧美| 一区二区亚洲精品国产| 国产视频久久久久| 亚洲国产精品免费| 国产精品免费久久久久影院| 97国产精品视频人人做人人爱| 久久久久久久久国产| 8050国产精品久久久久久| 97免费中文视频在线观看| 欧美黑人性视频| 欧美激情喷水视频| 亚洲国产精品一区二区久| 欧美一二三视频| 久久影视三级福利片| 视频一区视频二区国产精品| 国产91在线播放| 91亚洲国产成人久久精品网站| 国产成人精品在线| 欧美日韩激情美女| 精品无码久久久久久国产| 亚洲国产精品va在线看黑人| 国产日韩欧美自拍| 日韩av片免费在线观看| 亚洲女性裸体视频| 欧美色videos| 精品日本高清在线播放| 久色乳综合思思在线视频| 欧美精品在线免费观看| 亚洲精品一区二区在线| 久久久国产精品一区| 国产精品美女999| 日韩高清a**址| 欧美成人精品一区二区| 亚洲欧洲成视频免费观看| 日韩中文字幕在线免费观看| 精品毛片三在线观看| 夜夜嗨av一区二区三区四区| 欧美电影免费观看高清| 欧美激情xxxxx| 久久99视频精品| 久久人人看视频| 最近中文字幕日韩精品| 97视频免费观看| x99av成人免费| 69久久夜色精品国产69乱青草| 精品福利在线看| 亚洲成人黄色在线观看| 国产亚洲精品91在线| 91亚洲精华国产精华| 国产美女久久精品| 国产成人精品日本亚洲专区61| 91av视频在线免费观看| 国产午夜精品理论片a级探花| 欧美黑人一级爽快片淫片高清| 欧美激情女人20p| 亚洲国产97在线精品一区| 丝袜亚洲欧美日韩综合| 7m精品福利视频导航| 欧洲美女免费图片一区| 日韩欧美亚洲一二三区| 国内精品模特av私拍在线观看| 91在线观看欧美日韩| 日韩亚洲在线观看| 欧美激情一区二区久久久| 亚洲精品一区在线观看香蕉| 国产精品福利久久久| 国产成人拍精品视频午夜网站| 国产激情综合五月久久| 国产91精品不卡视频| 日本成人黄色片| 日韩欧美国产视频| 欧美视频在线观看免费网址| 欧美激情一区二区久久久| 亚洲美女在线观看| 国产一区二区三区在线观看视频| 国产欧美精品一区二区| 亚洲一区国产精品| 在线播放日韩精品| 亚洲第一精品夜夜躁人人爽| 国产精品pans私拍| 亚洲欧美国产高清va在线播| 亚洲免费成人av电影| 91黑丝在线观看| 国产亚洲精品久久久| 亚洲精品中文字幕有码专区| 4438全国亚洲精品在线观看视频| 亚洲美女精品成人在线视频| 久久久国产在线视频| 成人做爰www免费看视频网站| 欧美激情中文字幕乱码免费| 尤物tv国产一区| 久久久久亚洲精品国产| 91精品国产综合久久久久久蜜臀| 国产精品美女www爽爽爽视频| 亚洲精品国产拍免费91在线| 国产精品高清免费在线观看| 国产精品在线看| 亚洲精品小视频在线观看| 深夜福利国产精品| 久久天堂电影网| 欧美激情亚洲精品| 欧美最顶级丰满的aⅴ艳星| 亚洲免费电影一区| 91色精品视频在线| 久久免费视频这里只有精品| 国产精品偷伦视频免费观看国产| 欧美精品国产精品日韩精品| 尤物九九久久国产精品的特点| 欧美另类老肥妇| 亚洲精品mp4| …久久精品99久久香蕉国产| 亚洲毛片在线免费观看| 超碰日本道色综合久久综合| 国产婷婷成人久久av免费高清| 久久综合色影院| 中文字幕欧美日韩精品| 92国产精品久久久久首页| 欧美高清在线播放| 国内精品视频久久|