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組合到其它類中來使用,或者模擬類的實現和繼承,爲代碼解耦發力。

相關文章
相關標籤/搜索