JavaScript: 实现自定义事件

不管是从事web开发仍是从事GUI开发,事件都是咱们常常使用到的。事件又被称为观察者模式或订阅/发布,拿HTML来讲,一个DIV能够触发click事件,这个事件类型click是对外公开的,因此咱们能够去订阅它。若是经过DIV去订阅一个未知的事件类型,则其结果是未定义的。因此事件click在接受对外订阅以前,须要对外发布。当鼠标在DIV上点击时,click事件就被触发。javascript

jQuery的事件机制

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

<!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,使用大写的事件类型是一个约定,咱们不妨遵循这条规则好了。执行结果以下图所示:
java

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

实现自定义事件的思路

什么是发布事件

发布事件实际上是指定可用的事件类型列表。固然这个并不是必定要实现,相似jQuery方式的也是可行的。
web

什么是事件类型

事件类型实际上是至关于一个查找key,而这个key能够关联多个函数。因此这个事件类型应该是Map的一个key,这个key被关联到一个待执行函数列表。咱们暂且将这个Map定义为eventsList。
app

什么是事件订阅

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

什么是事件触发

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

事件机制的简单实现

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

<!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>

执行结果以下:spa

事件机制的完整实现

为何要先订阅再触发呢?由于订阅是往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组合到其它类中来使用,或者模拟类的实现和继承,为代码解耦发力。

相关文章
相关标签/搜索