文档章节

JavaScript: 实现自定义事件

陈亦
 陈亦
发布于 2014/02/16 02:33
字数 1799
阅读 2222
收藏 22

无论是从事web开发还是从事GUI开发,事件都是我们经常使用到的。事件又被称为观察者模式或订阅/发布,拿HTML来说,一个DIV可以触发click事件,这个事件类型click是对外公开的,所以我们可以去订阅它。如果通过DIV去订阅一个未知的事件类型,则其结果是未定义的。所以事件click在接受对外订阅之前,需要对外发布。当鼠标在DIV上点击时,click事件就被触发。

jQuery的事件机制

普通对象通过jQuery包装后即拥有自定义事件功能(当然拥有的功能非常多,但这里只关注自定义事件),并且jQuery的自定义事件被实现为无须对外发布事件即可被订阅。来看个例子:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Test Event</title>
    <meta name="author" content="" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <meta name="keywords" content="Test Event" />
    <meta name="description" content="Test Event" />
    <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script>
</head>
<body>
<script type="text/javascript">
var EventObject = jQuery({});
EventObject.bind('GO_TO_BED', function(event, name, hour) {
	console.group("Test Event");
	console.log("event object: ", event);
	console.log("name: ", name);
	console.log("hour: ", hour);
});
EventObject.trigger('GO_TO_BED', ['goal', 12]);
</script>
</body>
</html>

先bind,后trigger,这是有原因的,下文将详细解释这点。事件类型为GO_TO_BED,使用大写的事件类型是一个约定,我们不妨遵循这条规则好了。执行结果如下图所示:

在trigger时所传的参数被完整的传到bind时指定的事件句柄中,至于传参的方式,这只是实现上的细节。上述代码的bind是用于订阅事件,trigger用于触发事件。bind和trigger的第一个参数都是事件类型并且都是同一个事件类型才能被触发。而bind方法的第二个参数为GO_TO_BED事件被触发时所执行的函数。

实现自定义事件的思路

什么是发布事件

发布事件其实是指定可用的事件类型列表。当然这个并非一定要实现,类似jQuery方式的也是可行的。

什么是事件类型

事件类型其实是相当于一个查找key,而这个key可以关联多个函数。所以这个事件类型应该是Map的一个key,这个key被关联到一个待执行函数列表。我们暂且将这个Map定义为eventsList。

什么是事件订阅

事件订阅是往eventsList里添加事件类型key和它所关联的待执行函数。当然如果eventsList里已经存在某个key,则仅仅是将待执行函数添加到队列尾。

什么是事件触发

事件触发令所指定的事件类型key所关联的待执行函数列表有机会逐一执行。

事件机制的简单实现

为了对自定义事件机制有个大概的印象,下面简单实现了一个,只包括发布事件、订阅事件和触发事件功能。而且在订阅事件和触发事件时并没有去检测有没有公开相应的事件类型。代码如下:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Test Event</title>
    <meta name="author" content="" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <meta name="keywords" content="Test Event" />
    <meta name="description" content="Test Event" />
    <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script>
</head>
<body>
<input type="button" value="Test Event" />
<script type="text/javascript">
// 事件类
function Observer()
{
	this._eventsList = {}; // {'eat' : [{fn : null, scope : null}, {fn : null, scope : null}]}
}

Observer.prototype = {
	dispatchEvent : function(eName)
	{
		eName = eName.toLowerCase();
		this._eventsList[eName] = [];
	},
	on : function(eName, fn, scope)
	{
		eName = eName.toLowerCase();
		this._eventsList[eName].push({fn : fn || null, scope : scope || null});
	},
	fireEvent : function()
	{
		var args  = Array.prototype.slice.call(arguments);
		var eName = args.shift();
		eName = eName.toLowerCase();
		var list = this._eventsList[eName];
		for (var i = 0; i < list.length; i++)
		{
			var dict  = list[i];
			var fn    = dict.fn;
			var scope = dict.scope;
			fn.apply(scope || null, args);
		}
	}
};
// end

var EventObject = new Observer();

EventObject.dispatchEvent('GO_TO_BED');
EventObject.on('GO_TO_BED', function(name, hour) {
	console.group('Test Event');
	console.log(name + '要在' + hour + '点之前去睡觉');
});

~function($) {
	$(function() {
		$("input").click(function(event) {
			event.stopPropagation();
			EventObject.fireEvent('GO_TO_BED', 'goal', 12);
		});
	});
}(jQuery)
</script>
</body>
</html>

执行结果如下:

事件机制的完整实现

为什么要先订阅再触发呢?因为订阅是往eventsList添加key和可执行函数列表,如果颠倒了顺序,则在触发事件时eventsList中事件类型key所关联的可执行函数列表是空的,也就没什么可执行的了。下面是一个比较完整的实现:

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8">
    <title>Test Event</title>
    <meta name="author" content="" />
    <meta http-equiv="X-UA-Compatible" content="IE=7" />
    <meta name="keywords" content="Test Event" />
    <meta name="description" content="Test Event" />
    <script type="text/javascript" src="http://cdn.staticfile.org/jquery/2.1.0/jquery.min.js"></script>
</head>
<body>
<input type="button" value="Test Event" />
<script type="text/javascript">
/** 
* 观察者模式实现事件监听
*/
function Observer()
{
	this._eventsList = {}; // 对外发布的事件列表{"connect" : [{fn : null, scope : null}, {fn : null, scope : null}]}
}

Observer.prototype = {
	// 空函数
	_emptyFn : function()
	{
	},
	
	/**
	* 判断事件是否已发布
	* @param eType 事件类型
	* @return Boolean
	*/
	_hasDispatch : function(eType)
	{
		eType = (String(eType) || '').toLowerCase();

		return "undefined" !== typeof this._eventsList[eType];
	},
	
	/**
	* 根据事件类型查对fn所在的索引,如果不存在将返回-1
	* @param eType 事件类型
	* @param fn 事件句柄
	*/
	_indexFn : function(eType, fn)
	{
		if(!this._hasDispatch(eType))
		{
			return -1;
		}

		var list = this._eventsList[eType];
		fn = fn || '';
		for(var i = 0; i < list.length; i++)
		{
			var dict = list[i];
			var _fn  = dict.fn || '';
			if(fn.toString() === _fn.toString())
			{
				return i;
			}
		}

		return -1;
	},

	/**
	* 创建委托
	*/
	createDelegate : function()
	{
		var __method = this;
    	var args     = Array.prototype.slice.call(arguments);
    	var object   = args.shift();
    	return function() {
        	return __method.apply(object, args.concat(Array.prototype.slice.call(arguments)));
		}
	},
	
	/**
	* 发布事件
	*/
	dispatchEvent : function()
	{
		if(arguments.length < 1)
		{
			return false;
		}

		var args = Array.prototype.slice.call(arguments), _this = this;
		$.each(args, function(index, eType){
			if(_this._hasDispatch(eType))
			{
				return true;
			}
			_this._eventsList[eType.toLowerCase()] = [];
		});

		return this;
	},
	
	/**
	* 触发事件
	*/
	fireEvent : function()
	{
		if(arguments.length < 1)
		{
			return false;
		}

		var args = Array.prototype.slice.call(arguments), eType = args.shift().toLowerCase(), _this = this;
		if(this._hasDispatch(eType))
		{
			var list = this._eventsList[eType];
			if (!list)
			{
				return this;
			}

			$.each(list, function(index, dict){
				var fn = dict.fn, scope = dict.scope || _this;
				if(!fn || "function" !== typeof fn)
				{
					fn = _this._emptyFn;
				}
				if(true === scope)
				{
					scope = null;
				}

				fn.apply(scope, args);
			});
		}

		return this;
	},
	
	/**
	* 订阅事件
	* @param eType 事件类型
	* @param fn 事件句柄
	* @param scope
	*/
	on : function(eType, fn, scope)
	{
		eType = (eType || '').toLowerCase();
		if(!this._hasDispatch(eType))
		{
			throw new Error("not dispatch event " + eType);
			return false;
		}

		this._eventsList[eType].push({fn : fn || null, scope : scope || null});

		return this;
	},
	
	/**
	* 取消订阅某个事件
	* @param eType 事件类型
	* @param fn 事件句柄
	*/
	un : function(eType, fn)
	{
		eType = (eType || '').toLowerCase();
		if(this._hasDispatch(eType))
		{
			var index = this._indexFn(eType, fn);
			if(index > -1)
			{
				var list = this._eventsList[eType];
				list.splice(index, 1);
			}
		}

		return this;
	},
	
	/**
	* 取消订阅所有事件
	*/
	die : function(eType)
	{
		eType = (eType || '').toLowerCase();
		if(this._eventsList[eType])
		{
			this._eventsList[eType] = [];
		}

		return this;
	}
};
// end

var EventObject = new Observer();

EventObject.dispatchEvent('GO_TO_BED');
EventObject.on('GO_TO_BED', function(name, hour) {
	console.group('Test Event');
	console.log(name + '要在' + hour + '点之前去睡觉,谁又懂得了码农的辛酸啊?');
});

~function($) {
	$(function() {
		$("input").click(function(event) {
			event.stopPropagation();
			EventObject.fireEvent('GO_TO_BED', 'goal', 12);
		});
	});
}(jQuery)
</script>
</body>
</html>

以上代码完整的实现了发布事件、订阅事件、触发事件以及取消订阅功能。执行结果如下:

结束语

在有需要的时候可以将EventObject组合到其它类中来使用,或者模拟类的实现和继承,为代码解耦发力。

© 著作权归作者所有

陈亦
粉丝 241
博文 23
码字总数 53194
作品 0
浦东
高级程序员
私信 提问
「React Native」与「Android」的交互方式总结

React Native 作为一个混合开发解决方案,因为业务、性能上的种种原因,总是避免不了与原生进行交互。在开发过程中我们将 RN 与原生交互的几种方式进行了梳理,按照途径主要分为以下几类: ...

WirelessSprucetec
06/16
0
0
Vue进阶(三十):vue中使用element-ui进行表单验证

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/sunhuaqiang1/article/details/85235441 一、简单逻辑验证(直接使用rules) 实现思路 html中给el-form增加 ...

Lo秀娴ve
2018/12/24
0
0
WKWebView与js交互之完美解决方案

 随着H5功能愈发的强大,没进行过混合开发的小伙们都不好意思说自己能够独立进行iOS的app开发,在iOS7操作系统下,常用的native,js交互框架有easy-js,WebViewJavascriptBridge,以及结合jav...

HeroHY
2017/05/15
30
0
说说在 Vue.js 中如何实现组件间通信(高级篇)

之前说过,可以使用 props 将数据从父组件传递给子组件。其实还有其它种的通信方式,下面我们一一娓娓道来。 1 自定义事件 通过自定义事件,我们可以把数据从子组件传输回父组件。子组件通过...

deniro
2018/12/09
0
0
asp.net中调用javascript自定义函数的方法(包括引入JavaScript文件)总结

通常javascript代码可以与HTML标签一起直接放在前端页面中,但如果JS代码多的话一方面不利于维护,另一方面也对搜索引擎不友好,因为页面因此而变得臃肿;所以一般有良好开发习惯的程序员都会...

黄献
2012/11/04
952
0

没有更多内容

加载失败,请刷新页面

加载更多

聊聊Tomcat中的连接器(Connector)

上期回顾 上一篇文章《Tomcat在SpringBoot中是如何启动的》从main方法启动说起,窥探了SpringBoot是如何启动Tomcat的,在分析Tomcat中我们重点提到了,Tomcat主要包括2个组件,连接器(Conne...

木木匠
56分钟前
3
0
OSChina 周一乱弹 —— 熟悉的味道,难道这就是恋爱的感觉

Osc乱弹歌单(2019)请戳(这里) 【今日歌曲】 @xiaoshiyue :好久没分享歌了分享张碧晨的单曲《今后我与自己流浪》 《今后我与自己流浪》- 张碧晨 手机党少年们想听歌,请使劲儿戳(这里)...

小小编辑
今天
1K
20
SpringBoot中 集成 redisTemplate 对 Redis 的操作(二)

SpringBoot中 集成 redisTemplate 对 Redis 的操作(二) List 类型的操作 1、 向列表左侧添加数据 Long leftPush = redisTemplate.opsForList().leftPush("name", name); 2、 向列表右......

TcWong
今天
34
0
排序––快速排序(二)

根据排序––快速排序(一)的描述,现准备写一个快速排序的主体框架: 1、首先需要设置一个枢轴元素即setPivot(int i); 2、然后需要与枢轴元素进行比较即int comparePivot(int j); 3、最后...

FAT_mt
昨天
4
0
mysql概览

学习知识,首先要有一个总体的认识。以下为mysql概览 1-架构图 2-Detail csdn |简书 | 头条 | SegmentFault 思否 | 掘金 | 开源中国 |

程序员深夜写bug
昨天
12
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部