jQuery技巧之让任何组件都支持类似DOM的事件管理


Posted in Javascript onApril 05, 2016

本文介绍一个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];//第一个参数转为type
type = $.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;
});

实际调用测试如下:

jQuery技巧之让任何组件都支持类似DOM的事件管理
jQuery技巧之让任何组件都支持类似DOM的事件管理

测试中,实例化了一个FileUploadBaseView对象f,并设置了它的name属性,通过on方法添加一个跟hello相关的监听器,最后通过trigger方法触发了hello的监听器,并传递了额外的两个参数,在监听器内部除了可以通过监听器的函数参数访问到trigger传递过来的数据,还能通过this访问f对象。

从目前的结果来说,这个方式看起来还不错,但是在我想要继续实现FileUploadBaseView的时候碰到了问题。你看我在设计这个组件的时候那几个订阅相关的option:

jQuery技巧之让任何组件都支持类似DOM的事件管理 

我原本的设计是:这些订阅都是成对定义,一对订阅跟某个实例方法对应,比如带before的那个订阅会在相应的实例方法(render)调用前触发,不带before的那个订阅会在相应的实例方法(render)调用后触发,而且还要求带before的那个订阅如果返回false,就不执行相应的实例方法以及后面的订阅。最后这个设计要求是考虑到在调用组件的实例方法之前,有可能因为一些特殊的原因,必须得取消当前实例方法的调用,比如调用remove方法时有的数据不能remove,那么就可以在before订阅里面做一些校验,能删除的返回true,不能删除的返回false,然后在实例方法中触发before的订阅后加一个判断就可以了,类似下面的这种做法:

jQuery技巧之让任何组件都支持类似DOM的事件管理

但是这个做法只能在单纯的回调函数模式里实现,在发布-订阅模式下是行不通的,因为回调函数只会跟一个函数引用相关,而发布-订阅模式里,同一个消息可能有多个订阅,如果把这种做法应用到发布-订阅里面,当调用this.trigger('beforeRender')的时候,会把跟beforeRender关联的所有订阅全部调用一次,那么以哪个订阅的返回值为准呢?也许你会说可以用队列中的最后一个订阅的返回值为准,在大多数情况下也许这么干没问题,但是当我们把“以队列最后的一个订阅返回值作为判断标准”这个逻辑加入到EventBase中的时候,会出现一个很大的风险,就是外部在使用的时候,一定得清楚地管理好订阅的顺序,一定要把那个跟校验等一些特殊逻辑相关的订阅放在最后面才行,而这种跟语法、编译没有关系,对编码顺序有要求的开发方式会给软件带来比较大的安全隐患,谁能保证任何时候任何场景都能控制好订阅的顺序呢,更何况公司里面可能还有些后来的新人,压根不知道你写的东西还有这样的限制。

解决这个问题的完美方式,就是像DOM对象的事件那样,在消息发布的时候,不是简简单单的发布一个消息字符串,而是把这个消息封装成一个对象,这个对象会传递给它所有的订阅,哪个订阅里觉得应该阻止这个消息发布之后的逻辑,只要调用这个消息的preventDefault()方法,然后在外部发布完消息后,调用消息的isDefaultPrevented()方法判断一下即可:

jQuery技巧之让任何组件都支持类似DOM的事件管理

而这个做法跟使用jquery管理DOM对象的事件是一样的思路,比如bootstrap的大部分组件以及我在前面一些博客中写的组件都是用的这个方法来增加额外的判断逻辑,比如bootstrap的alert组件在close方法执行的时候有一段这样的判断:

jQuery技巧之让任何组件都支持类似DOM的事件管理

按照这个思路去改造EventBase是一个解决问题的方法,但是jquery的一个小技巧,能够让我们把整个普通对象的事件管理变得更加简单,下面就让我们来瞧一瞧它的庐山真面目。

2. jquery小技巧模式

1)技巧一

如果在定义组件的时候,这个组件是跟DOM对象有关联的,比如下面这种形式:

jQuery技巧之让任何组件都支持类似DOM的事件管理

那么我们可以完全给这个组件添加on off trigger one这几个常用事件管理的方法,然后将这些方法代理到$element的相应方法上:

jQuery技巧之让任何组件都支持类似DOM的事件管理

通过代理,当调用组件的on方法时,其实调用的是$element的on方法,这样的话这种类型的组件就能支持完美的事件管理了。

2)技巧二

第一个技巧只能适用于跟DOM有关联的组件,对于那些跟DOM完全没有关联的组件该怎么添加像前面这样完美的事件管理机制呢?其实方法也很简单,只是我自己以前真的是没这么用过,所以这一次用起来才会觉得特别新鲜:

jQuery技巧之让任何组件都支持类似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元素,最后的测试结果是:

完全与预期一致。

jQuery技巧之让任何组件都支持类似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()的调用,而且该回调还不是最后一个,最后的测试结果是:

jQuery技巧之让任何组件都支持类似DOM的事件管理

从结果可以看到,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()的调用,而且该回调还不是最后一个,最后的测试结果是:

jQuery技巧之让任何组件都支持类似DOM的事件管理

从结果可以看到,render方法的主要逻辑代码跟后面的render事件都没有执行,所有beforeRender的监听器都执行了,说明e.preventDefault()生效了,而且它没有对beforeRender的事件队列产生影响。

所以从2个测试来看,通过改造后的EventBase,我们得到了一个可以让任意对象支持jquery事件管理机制的方法,将来在考虑用事件机制来解耦的时候,就不用再去考虑前面第一个介绍的发布-订阅模式了,而且相对而言这个方法功能更强更稳定,也更符合你平常使用jquery操作DOM的习惯。

4. 本文小结

有2点需要再说明一下的是:

1)即使不用jquery按照第1部分最后提出的思路,把第一部分常规的发布-订阅模式改造一下也可以的,只不过用jquery更加简洁些;

2)最终用jquery 的事件机制来实现任意对象的事件管理,一方面是用到了代理模式,更重要的还是要用发布-订阅模式,只不过最后的这个实现是由jquery帮我们把第一部分的发布-订阅实现改造好了而已。

以上内容是针对jQuery技巧之让任何组件都支持类似DOM的事件管理的相关知识,希望对大家有所帮助!

Javascript 相关文章推荐
js的压缩及jquery压缩探讨(提高页面加载性能/保护劳动成果)
Jan 29 Javascript
jquery插件jquery倒计时插件分享
Dec 27 Javascript
$.each与$().each的区别示例介绍
Mar 20 Javascript
jquery实现通用版鼠标经过淡入淡出效果
Jun 15 Javascript
Bootstrap基础学习
Jun 16 Javascript
JavaScript实现点击单选按钮改变输入框中文本域内容的方法
Aug 12 Javascript
JQuery异步加载PartialView的方法
Jun 07 Javascript
jQuery插件 Jqplot图表实例
Jun 18 Javascript
vue 数组和对象不能直接赋值情况和解决方法(推荐)
Oct 25 Javascript
浅谈高大上的微信小程序中渲染html内容—技术分享
Oct 25 Javascript
微信小程序使用swiper组件实现层叠轮播图
Nov 04 Javascript
浅谈JavaScript节流和防抖函数
Aug 25 Javascript
JS+CSS实现闪烁字体效果代码
Apr 05 #Javascript
js拖拽的原型声明和用法总结
Apr 04 #Javascript
javascript如何实现360度全景照片问题汇总
Apr 04 #Javascript
javascript制作照片墙及制作过程中出现的问题
Apr 04 #Javascript
javascript拖拽效果延伸学习
Apr 04 #Javascript
javascript事件委托的用法及其好处简析
Apr 04 #Javascript
基于javascript制作微博发布栏效果
Apr 04 #Javascript
You might like
星际争霸 Starcraft 游戏介绍
2020/03/14 星际争霸
phpinfo 系统查看参数函数代码
2009/06/05 PHP
PHP中使用php5-ffmpeg撷取视频图片实例
2015/01/07 PHP
Yii2中设置与获取别名的函数(setAlias和getAlias)用法分析
2016/07/25 PHP
Laravel程序架构设计思路之使用动作类
2018/06/07 PHP
Laravel框架实现多数据库连接操作详解
2019/07/12 PHP
菜鸟学习JavaScript小实验之函数引用
2010/11/17 Javascript
javascript的解析执行顺序在各个浏览器中的不同
2014/03/17 Javascript
10分钟学会写Jquery插件实例教程
2014/09/06 Javascript
jquery实现一个简单的表单验证实例
2016/03/30 Javascript
jQuery实现加入收藏夹功能(主流浏览器兼职)
2016/12/24 Javascript
vue2.0 keep-alive最佳实践
2017/07/06 Javascript
jQuery实现鼠标响应式淘宝动画效果示例
2018/02/13 jQuery
vue template中slot-scope/scope的使用方法
2018/09/06 Javascript
Vue入门学习笔记【基本概念、对象、过滤器、指令等】
2019/04/13 Javascript
浅谈Node 异步IO和事件循环
2019/05/05 Javascript
在Python中使用AOP实现Redis缓存示例
2017/07/11 Python
Python图像处理之gif动态图的解析与合成操作详解
2018/12/30 Python
使用python turtle画高达
2020/01/19 Python
Pytorch数据拼接与拆分操作实现图解
2020/04/30 Python
CSS3移动端vw+rem不依赖JS实现响应式布局的方法
2019/01/23 HTML / CSS
HTML5通用接口详解
2016/06/12 HTML / CSS
Canvas系列之滤镜效果
2019/02/12 HTML / CSS
Everything But Water官网:美国泳装品牌
2019/03/17 全球购物
ASICS印度官方网站:日本专业运动品牌
2020/06/20 全球购物
C/C++程序员常见面试题二
2015/11/19 面试题
大学生职业生涯规划书参考模板
2014/03/05 职场文书
人力资源管理系自荐信
2014/05/31 职场文书
应届大学生求职信
2014/07/20 职场文书
发布会邀请函
2015/01/31 职场文书
高中开学感言
2015/08/01 职场文书
贴吧吧主申请感言
2015/08/03 职场文书
教师旷工检讨书
2015/08/15 职场文书
公司周年庆寄语
2019/06/21 职场文书
用Python爬虫破解滑动验证码的案例解析
2021/05/06 Python
Oracle11g R2 安装教程完整版
2021/06/04 Oracle