亚洲乱码中文字幕综合,中国熟女仑乱hd,亚洲精品乱拍国产一区二区三区,一本大道卡一卡二卡三乱码全集资源,又粗又黄又硬又爽的免费视频

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

 更新時(shí)間:2016年04月05日 09:37:51   投稿:mrr  
這篇文章主要介紹了jQuery技巧之讓任何組件都支持類似DOM的事件管理 的相關(guān)資料,需要的朋友可以參考下

本文介紹一個(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)文章

最新評(píng)論