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 相关文章推荐
复制小说文本时出现的随机乱码的去除方法
Sep 07 Javascript
Dom与浏览器兼容性说明
Oct 25 Javascript
JavaScript异步回调的Promise模式封装实例
Jun 07 Javascript
angularjs学习笔记之简单介绍
Sep 26 Javascript
详解JavaScript的流程控制语句
Nov 30 Javascript
基于JavaScript实现全屏透明遮罩div层锁屏效果
Jan 26 Javascript
深入理解jquery跨域请求方法
May 18 Javascript
javascript入门之string对象【新手必看】
Nov 22 Javascript
微信小程序技巧之show内容展示,上传文件编码问题
Jan 23 Javascript
Vue-cli配置打包文件本地使用的教程图解
Aug 02 Javascript
微信小程序实现的canvas合成图片功能示例
May 03 Javascript
js实现踩五彩块游戏
Feb 08 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
php 高效率写法 推荐
2010/02/21 PHP
DEDE实现转跳属性文档在模板上调用出转跳地址
2016/11/04 PHP
详解PHP swoole process的使用方法
2017/08/26 PHP
php实现每日签到功能
2018/11/29 PHP
laravel-admin自动生成模块,及相关基础配置方法
2019/10/08 PHP
Prototype中dom对象方法汇总
2008/09/17 Javascript
mysql输出数据赋给js变量报unterminated string literal错误原因
2010/05/22 Javascript
JS实现网页标题栏显示当前时间和日期的完整代码
2015/11/02 Javascript
AngularJS教程之MVC体系结构详解
2016/08/16 Javascript
微信小程序 页面跳转传递值几种方法详解
2017/01/12 Javascript
20行js代码实现的贪吃蛇小游戏
2017/06/20 Javascript
Vuejs+vue-router打包+Nginx配置的实例
2018/09/20 Javascript
Node 搭建一个静态资源服务器的实现
2019/05/20 Javascript
vue 解决异步数据更新问题
2019/10/29 Javascript
JS箭头函数和常规函数之间的区别实例分析【 5 个区别】
2020/05/27 Javascript
基于Vue.js+Nuxt开发自定义弹出层组件
2020/10/09 Javascript
Vue路由权限控制解析
2020/11/09 Javascript
[19:54]夜魇凡尔赛茶话会 第一期02:看图识人
2021/03/11 DOTA
python 多线程串行和并行的实例
2019/02/22 Python
python调用摄像头拍摄数据集
2019/06/01 Python
基于Django静态资源部署404的解决方法
2019/07/28 Python
Python学习笔记之Zip和Enumerate用法实例分析
2019/08/14 Python
基于python实现把图片转换成素描
2019/11/13 Python
Python线程threading模块用法详解
2020/02/26 Python
Jupyter Notebook 实现正常显示中文和负号
2020/04/24 Python
使用Keras实现简单线性回归模型操作
2020/06/12 Python
Django中ORM的基本使用教程
2020/12/22 Python
python asyncio 协程库的使用
2021/01/21 Python
css3的@media属性实现页面响应式布局示例代码
2014/02/10 HTML / CSS
迟到检讨书900字
2014/01/14 职场文书
校庆标语集锦
2014/06/25 职场文书
我的中国梦演讲稿400字
2014/08/19 职场文书
列车乘务员工作不细心检讨书
2014/10/07 职场文书
2014年副班长工作总结
2014/12/10 职场文书
八一建军节主持词
2015/07/01 职场文书
几款流行的HTML5 UI框架比较(小结)
2021/04/08 HTML / CSS