js事件详解


一、监听事件


绑定事件,ie有attachEvent,w3c有addEventListener,
解除绑定,ie有detachEvent,w3c有removeEventListener

jQuery做了很好的封装,api也比较多,bind,live,delegate,on等等,其不同api也所区别,由于版本问题,个别api也有所区别。我个人一般都用同一个,要么用bind,要么用on。

我们也可以自己封装,代码如下:

<span style="font-size:14px;">var EventUtil ={
	addHandler: function(element, type, handler){
		//w3c
		if(element.addEventListener){
			element.addEventListener(type, handler, false);
		}
		//ie
		else if(element.attachEvent){
			element.attachEvent('on' + type,handler);
		}else{
			element['on' + type] = handler;
		}
	},
	removeHandler: function(){
		if(element.removeEventListener){
			element.removeEventListener(type, handler, false);
		}else if(element.detachEvent){
			element.detachEvent('on' + type, handler);
		}else{
			element['on' + type] = null;
		}
	}
}</span>


使用方法:

<span style="font-size:14px;">var btn = document.getElementById('mybtn');
var handler = function(){
	alert("111")
};
//add click
EventUtil.addHandler(btn, 'click', handler);
//remove click
EventUtil.removeHandler(btn, 'click', handler);</span>





二、冒泡机制和捕获机制


ie用的是冒泡机制,w3c都支持的。
element.addEventListener(type, handler, false)这句代码里的第三个参数就是说明是否使用捕获机制,一般我们都使用冒泡的。至于捕获机制,我个人目前尚未遇到具体的使用场景。
那啥叫冒泡机制和捕获机制呢?
看这样一种情况,父元素是ul,子元素是li,两个元素都绑定点击事件。如果我们点击了li,li的点击事件当然被触发了,那么ul是不是也触发了呢,答案是肯定的。那么就涉及到一个问题。谁先触发的问题。
冒泡机制是指先触发子元素的click事件,然后再触发父元素的click事件,是由内向外的。
捕获机制就是从外向内传播的。先触发父节点的点击事件,然后触发子节点的点击事件。


示例1:ie 678下,点击li触发,弹出顺序是li,ul,document

<html>
<body>
<ul>
	<li ></li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.attachEvent('onclick',function(){alert('document')})
ul.attachEvent('onclick',function(){alert('ul')});
li.attachEvent('onclick',function(){alert('li')});
</script>
</body>
</html>

示例2:chrome下,点击li触发,弹出顺序是document,ul,li

<!doctype html>
<html>
<head>
<style>
ul{
	background : gray;
	padding : 20px;
}
ul li{
	background : green;
}
</style>
</head>
<body>
<ul>
	<li ></li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
var li = document.getElementsByTagName('li')[0];
document.addEventListener('click',function(){alert('document')},true);//第三个参数为true使用捕获
ul.addEventListener('click',function(){alert('ul')},true);
li.addEventListener('click',function(){alert('li')},true);
</script>
</body>
</html>

如果要阻止冒泡,我们也可以封装一下:

function stopBubble(e) { 
//如果提供了事件对象,则这是一个非IE浏览器 
if ( e && e.stopPropagation ) 
    //因此它支持W3C的stopPropagation()方法 
    e.stopPropagation(); 
else 
    //否则,我们需要使用IE的方式来取消事件冒泡 
    window.event.cancelBubble = true; 
}


当我们需要阻止浏览器默认事件的时候:


<span style="font-size:14px;">//阻止浏览器的默认行为 
function stopDefault( e ) { 
    //阻止默认浏览器动作(W3C) 
    if ( e && e.preventDefault ) 
        e.preventDefault(); 
    //IE中阻止函数器默认动作的方式 
    else 
        window.event.returnValue = false; 
    return false; 
}</span>





三、事件委托


js委托是什么呢,因为冒泡机制,既然点击子元素时,也会触发父元素的点击事件。那么我们为什么不把点击子元素的事件要做的事情,写到父元素的事件里呢?谁第一个想出来的,真叫人佩服。

我们来看下面一个示例:w3c下使用:

<!doctype html>
<html>
<head>
<style>
ul{
	background : gray;
	padding : 20px;
}
ul li{
	background : green;
	margin :5px;
}
</style>
</head>
<body>
<ul>
	<li ></li>
	<li ></li>
	<li ></li>
	<li ></li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
ul.addEventListener('click',function(e){
	console.dir(e.target.tagName)
	if(e.target.tagName=='LI'){
		alert('li');
	}else{
		alert('outside of li')
	}
},false);
</script>
</body>
</html>


再来看一下事件委托于父节点的示例:

<!doctype html>
<html>
<head>
<style>
ul{
	background : gray;
	padding : 20px;
}
ul li{
	background : green;
	margin :5px;
}
</style>
</head>
<body>
<ul>
	<li ></li>
	<li ></li>
	<li ></li>
	<li ></li>
</ul>
<script>
var ul = document.getElementsByTagName('ul')[0];
ul.addEventListener('mouseover',function(e){
	if(e.target.tagName=='LI'){
		var target = e.target;
		target.style.background = "yellow";
	}
},false);
ul.addEventListener('mouseout',function(e){
	if(e.target.tagName=='LI'){
		var target = e.target;
		target.style.background = "green";
	}
},false);
</script>
</body>
</html>

 这里再举个比较常见例子,使用场景是这样的,我们经常要使用一些表格,来展示数据,一般最后一列都是数据相关操作,比如查看、修改和删除等操作。这时也可以使用委托。

<!doctype html>
<html>
<head>
</head>
<body>
<div id = "oper">
<a href="#" name="info">showDetails</a>
<a href="#" name="update">update</a>
<a href="#" name="delete">delete</a>
<div>
<script>
var oper = document.getElementById('oper');
oper.addEventListener('click',function(e){
	var target = e.target;
	var name = null;
	if(target){
		name = target.name;
	}
	if(!name){
		return ;
	}
	if(name == 'info'){
		alert('showDetails');
	}else if(name== 'update'){
		alert('update');
	}else if(name == 'delete'){
		alert('delete');
	}
},false);
</script>
</body>
</html>

这里先总结一下js委托相关。
1.好处大大的,因为把事件绑定到父节点上了,因此事件少了很多。就算新增的子节点自然也有了相关事件。删除部分子节点不用你销毁对应节点上绑定的事件。
2.父节点是通过event.target来找到对应的子节点的。



四、自定义事件


ok,先来看看jquery是怎么实现自定义事件的
jquery中用on或bind绑定自定义事件,用trigger来触发
示例

<!doctype html>
<html>
<head>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function(){
	$('button').on('mycustomEvent',function(){
		alert("trigger customEvent");
	});
	$('button').trigger('mycustomEvent');
});
</script>
</head>
<body>
<button style="width:100px;height:20px;"></button>
</body>
</html>

jquery是怎么用的,先看到这,这时要来说一个关键的事情。必须得解释一下什么叫自定义事件。
一旦你理解其精髓,你的js世界观就会变了,会感觉到整个天空都亮了(且听我胡说)。
先来看一下什么叫事件呢?不用去找什么定义,自己闭眼睛想想就知道。用户要跟浏览器交互,浏览器要做出反应。这个反应就是事件。
以button绑定onclick为例,我们来看看事件的组成要素
第一,谁的事件,button的
第二,谁来触发,用户点击按钮时触发事件,可以说是用户来触发的
第三,谁来执行,button来执行,所以对应函数执行环境(执行上下文)this是指向button的。
第四,要做什么事情,要做的事情就是函数的执行。
第五,既然是函数,那么参数是什么,是event。那event是什么?event是事件的状态对象。
细心的我们,有没有发现一件事情,函数是整个事件中最重要的组成部分。下面来打通任督二脉。

事件==函数

来看一下函数的组成部分
为了方便说,举个例子

var obj ={
	fn : function(){
		console.log(this);
		console.log(arguments);
	}
}
obj.fn();
第一,谁的函数,obj的
第二,谁来触发,函数调用来触发的,或者说是代码obj.fn()来触发的
第三,谁来执行,obj来执行,因此对应函数的this是指向obj的。
第四,要做什么事情,不必说,
第五,既然是函数,那么参数是什么,也不必说。

其核心的地方就是触发等价于调用。再换个角度想想,事件不就是要做一些事情吗,而函数正是做一些事情的封装,二者肯定脱不了干系的。
那么什么叫自定义事件,狭义的来说,给dom元素绑定一些非浏览器默认支持的事件。广义的来说呢?要反过来来看上面的等式
函数==事件
函数其实就是自定义的事件
所以大家看那个jquery自定义事件例子,初看没啥用,其实就相当于

var obj = $('button')
obj.customEvent = function(){
	alert("trigger customEvent");
}
obj.customEvent();

但是,事实关键不在于,代码写的如何,代码谁多谁少的问题。而是思维的转变,如果你把函数当成事件来看,看事情的角度就发生变化了。
为啥说js是事件驱动的一门语言。现在能更好的理解这句话了。

扯远了,回到jquery自定义事件。
先看一下全选的例子,可以直接看运行效果。下面是直觉思维的实现方式。

<!doctype html>
<html>
<head>
<style>
body{
	margin : 30px;
}
#container{
	border : 1px solid black;
	width : 200px;
	height :200px;
}
</style>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>

</script>
<script>
$(function(){
	//绑定按钮点击事件
	$('#add').on('click',function(){
		//看看是不是全选
		if($('#selectAll').is(':checked')){
			$('#container').append(' <input type="checkbox" checked="checked"/>');
		}else{
			$('#container').append(' <input type="checkbox" />');
		}
	});
	//全选逻辑
	$('#selectAll').on('click',function(){
		if(this.checked){
			$('#container').find('input:checkbox').prop('checked','checked');
		}else{
			$('#container').find('input:checkbox').removeProp('checked');
		}
	});
})
</script>
</head>
<body>
<input id="selectAll" type="checkbox"/> <button id="add">add</button>
<div id="container">
<input type="checkbox"/>
<input type="checkbox"/>
<input type="checkbox"/>
<input type="checkbox"/>
</div>
</body>
</html>

从上面可以看出来
add要和container以及selectAll打交道
selectAll要和container打交道
如果把这三者,看做是三个模块,那么可以说这三者强耦合在一起了。

下面换一种方式来做
此时自定义事件出马,只让add、selectAll分别和container打交道

$(function(){
	var $container =$('#container');
	//绑定两个自定义事件
	$container.on('selectCheckbox',function(e,checked){
		//缓存状态
		var $this = $(this).data('state',checked);
		if(checked){
			$this.find('input:checkbox').prop('checked','checked');
		}else{
			$this.find('input:checkbox').removeProp('checked');
		}
	}).on('addCheckbox',function(){
		var $this = $(this);
		if($this.data('state')){
			$this.append(' <input type="checkbox" checked="checked"/>');
		}else{
			$this.append(' <input type="checkbox" />');
		}
	});
	//全选按钮触发自定义事件
	$('#selectAll').on('click',function(){
		$container.trigger('selectCheckbox',[this.checked])
	});
	//新增按钮触发自定义事件
	$('#add').on('click',function(){
		$container.trigger('addCheckbox');
	});
})

情况好了一些,还不够彻底,能不能这三个模块一点也不发生耦合呢?
貌似很难,其实再多出一个模块就ok了(就这个例子而言,代码确实比最开始的实现多了很多,这里为了把道理说明白,真实大才小用了)

$(function(){
	var $common =$({});
	
	//绑定两个自定义事件
	$common.on('selectCheckbox',function(e,checked){
		//缓存状态(原先是缓存到container中的)
		var $this = $(this).data('state',checked);
		if(checked){
			$('#container').find('input:checkbox').prop('checked','checked');
		}else{
			$('#container').find('input:checkbox').removeProp('checked');
		}
	}).on('addCheckbox',function(){
		if($(this).data('state')){
			$('#container').append(' <input type="checkbox" checked="checked"/>');
		}else{
			$('#container').append(' <input type="checkbox" />');
		}
	});
	
	//全选按钮触发自定义事件
	$('#selectAll').on('click',function(){
		$common.trigger('selectCheckbox',[this.checked])
	});
	
	//新增按钮触发自定义事件
	$('#add').on('click',function(){
		$common.trigger('addCheckbox');
	});
})



五、发布订阅模式


在说发布订阅模式之前,让我们再重新看看委托。
这次再说委托可不是仅仅说由于冒泡机制的那个委托。
委托本质是啥呢?还是你句话,你要办的事情,不是自己直接去办,而是拜托别人去办。
那好,由于主宾不同,这就引出了两种思路。
我委托别人,那相当于我是借鸡下蛋。(直觉思维)
别人委托我,那相当于我为其他人做嫁衣。
那么我们就来看看怎么做的嫁衣、怎么下的蛋

var sum = function(a, b){
	return a + b;
}
var mysum = function(a, b){
	return sum(a, b);
}
alert(mysum(2,3));
mysum借用了sum下了蛋。
没错,这是函数调用,但我们要用委托的眼光来看我们的代码,由于角度不同,想法也许就会不同。
为了讲下去,把例子复杂一下
var operation = function(a,b,oper){
	var r = 0
	switch(oper){
		case '+': r = a+b;break;
		case '-': r = a-b;break;
		case '*': r = a*b;break;
		case '/': r = a/b;break;
		case '%': r = a%b;break;
		default : r = NAN;
	}
	return r;
}
var minus = function(a,b){
	return operation(a,b,'-');
}
alert(minus(4,2));


可以说写operation就是为minus做嫁衣的。代码虽简单其实这也是一种模式:门面模式。
你说直接要用operation来做减法,当然可以的,但是minus是不是更简单一些,更有语意化呢。
jquery中那可是大量用了这种方法,on是bind的门面,$.get是$.ajax的门面等等。

扯远了,继续下蛋。

var P = function(name){
	this.name = name;
}
var Man = function(name,sex){
	P.call(this,name);
	this.sex = sex;
}
var m = new Man('wushaoxion','male');
alert(m.name);

我们来看看 发布订阅模式是怎么做的:

<!doctype html>
<html>
<head>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
(function($){
	var o = $({});
	$.sub = function(){
		o.on.apply(o,arguments);
	}
	$.unsub = function(){
		o.off.apply(o,arguments);
	}
	$.pub = function(){
		o.trigger.apply(o,arguments);
	}
})(jQuery);
</script>
<script>
$.sub('customEvent',function(e,text){
	alert(text);
})
$.pub('customEvent',['oh yeah']);
$.pub('customEvent',['haha']);
$.unsub('customEvent');
$.pub('customEvent',['can not see']);
</script>
</head>
<body></body>
</html>
先来简单看看那几行代码 
给jQuery函数添加了三个静态函数,订阅、取消订阅、发布。函数内部通过on和trigger来实现的。是委托到$({})这个对象上的。
内部虽然是通过绑定自定义事件和触发自定义事件来实现的。自定义事件只不过就是个名字而已。我们可以按照官方来理解,把它看做是一个通道或者是主题,订阅我这个主题,我发消息你自然就能看到。

下面我们来改改全选的实现3那个例子。

<!doctype html>
<html>
<head>
<style>
body{
	margin : 30px;
}
#container{
	border : 1px solid black;
	width : 200px;
	height :200px;
}
</style>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
(function($){
	var o = $({});
	$.sub = function(){
		o.on.apply(o,arguments);
	}
	$.unsub = function(){
		o.off.apply(o,arguments);
	}
	$.pub = function(){
		o.trigger.apply(o,arguments);
	}
})(jQuery);
</script>
<script>
$(function(){	
	var currentChecked = false;
	//订阅主题
	$.sub('selectCheckbox',function(e,checked){
		currentChecked = checked;
		if(checked){
			$('#container').find('input:checkbox').prop('checked','checked');
		}else{
			$('#container').find('input:checkbox').removeProp('checked');
		}
	})
	//订阅主题
	$.sub('addCheckbox',function(){
		if(currentChecked){
			$('#container').append(' <input type="checkbox" checked="checked"/>');
		}else{
			$('#container').append(' <input type="checkbox" />');
		}
	});
	
	//全选按钮发布主题
	$('#selectAll').on('click',function(){
		$.pub('selectCheckbox',[this.checked])
	});
	
	//新增按钮发布主题
	$('#add').on('click',function(){
		$.pub('addCheckbox');
	});
})
</script>
</head>
<body>
<input id="selectAll" type="checkbox"/> <button id="add">add</button>
<div id="container">
<input type="checkbox"/>
<input type="checkbox"/>
<input type="checkbox"/>
<input type="checkbox"/>
</div>
</body>
</html>

原生js实现,有很多种实现方法,这里我简单写了一个,匿名函数还是删不掉的(如果要实现的话,得弄个uid才行)。

<!doctype html>
<html>
<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
var obj = (function(){
	return {
		topics : {},
		sub : function(topic,fn){
			if(!this.topics[topic]){
				this.topics[topic] = [];
			}
			this.topics[topic].push(fn);
		},
		pub : function(topic){
			var arr = this.topics[topic];
			var args = [].slice.call(arguments,1);
			if(!arr){
				return;
			}
			for(var i = 0, len = arr.length;i < len;i++){
				arr[i](args);
			}
		},
		unsub : function(topic,fn){
			var arr = this.topics[topic];
			if(!arr){
				return;
			}
			if(typeof fn !== 'function'){
				delete this.topics[topic];
				return;
			}
			for(var i = 0, len = arr.length;i < len;i++){
				if(arr[i] == fn){
					arr.splice(i,1);
				}
			}
		}
	};
})();
var fn1 = function(name){
	alert(name);
}
var fn2 = function(name){
	alert('name: '+name);
}

obj.sub('xxxx',fn1);
obj.sub('xxxx',fn2);
obj.pub('xxxx','123');

obj.unsub('xxxx',fn2);
obj.pub('xxxx','333');

obj.unsub('xxxx');
obj.pub('xxxx','222');
</script>
</head>

<body>
</body>
</html>



六、观察者模式


这里还是拿那个全选的例子来说说:
目标 : 全选复选框
观察者:container里的复选框,和add按钮
状态:全选按钮是否选上
行为:你选,container我就选上,点击add时,添加选上的按钮,反之,反然。那怎么实现呢?
这里有两种思路,我观察你,想看看你的状态,我可以到你那去看,还有一种就是你的状态变化时,你来告诉我。
说白了就是“取”和“送”的概念。
观察者模式是采用“送”的概念。
那也涉及到一个问题,既然是“送”,你怎么来找到我呢?那你得保存我给你的地址。

<!doctype html>
<html>
<head>
<style>
body{
	margin : 30px;
}
#container{
	border : 1px solid black;
	width : 200px;
	height :200px;
}
</style>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
$(function(){

	//缓存三个元素
	var $selectAll = $('#selectAll');
	var $add = $('#add');
	var $container = $('#container');
	
	//目标保存观察者
	$selectAll.cbList = [];
	$selectAll.addBtn = $add;
	
	//目标通知函数,通知观察者更新
	$selectAll.notify = function(value){
		$.each(this.cbList,function(index,checkbox){
			checkbox.update(value);
		});
		this.addBtn.update(value);
	};
	
	
	$selectAll.click(function(){
		$selectAll.notify(this.checked);
	})
	$add.click(function(){
		addCheckbox($add.data('checked'));
	});
	
	//观察者add的更新方法
	$add.update = function(value){
		this.data('checked',value);
	}
	function addCheckbox(flag){
		var $cb;
		if(flag){
			$cb = $('<input type="checkbox" checked="checked" >');
		}else{
			$cb = $('<input type="checkbox">');
		}
		//观察者复选框更新方法
		$cb.update = function(value){
			this[0].checked = value; 
		}
		
		//目标存入观察者
		$selectAll.cbList.push($cb);
		$('#container').append($cb);
	}
})
</script>
</head>
<body>
<input id="selectAll" type="checkbox"/> <button id="add">add</button>
<div id="container">
</div>
</body>
</html>

看到这里,难免会想这里哪有观察者模式的“模式”影子啊。
下来就要抽出目标和观察者的抽象接口,然后才是真正观察模式,才能得以复用。

<!doctype html>
<html>
<head>
<style>
body{
	margin : 30px;
}
#container{
	border : 1px solid black;
	width : 200px;
	height :200px;
}
</style>
<script src="http://libs.baidu.com/jquery/1.9.1/jquery.min.js"></script>
<script>
var ObserverPatten = (function(){
	var Subject = function(){
		this.observerlist = [];
	}; 
	Subject.prototype = {
		add : function(observer){
			this.observerlist.push(observer);
		},
		notify : function(state){
			var arr = this.observerlist;
			for(var i = 0, len = arr.length;i<len;i++){
				arr[i].update(state);
			}
		}
		/*一些查找、移除、统计、清空等等函数  省略了。。。*/
	};
	var Observer = function(){
	};
	Observer.prototype = {
		//等待被复写
		update : function(){},
		/*...*/
	};
	return {
		Subject : Subject,
		Observer : Observer
	};
})();


</script>
<script>
$(function(){


	//缓存三个元素
	var $selectAll = $('#selectAll');
	var $add = $('#add');
	var $container = $('#container');
	
	//具体目标实现目标接口
	$selectAll.extend(new ObserverPatten.Subject);
	$selectAll.add($add);
	$selectAll.click(function(){
		$selectAll.notify(this.checked);
	})
	
	$add.click(function(){
		addCheckbox($add.data('checked'));
	});
	
	//观察者add实现观察者接口
	$add.extend(new ObserverPatten.Observer);
	
	//复写update方法
	$add.update = function(value){
		this.data('checked',value);
	}
	function addCheckbox(flag){
		var $cb;
		if(flag){
			$cb = $('<input type="checkbox" checked="checked" >');
		}else{
			$cb = $('<input type="checkbox">');
		}
		
		//观察者复选框实现观察者接口
		$cb.extend(new ObserverPatten.Observer);
		
		//复写update方法
		$cb.update = function(value){
			this[0].checked = value; 
		}
		
		//目标存入观察者
		$selectAll.add($cb);
		$('#container').append($cb);
	}
})
</script>
</head>
<body>
<input id="selectAll" type="checkbox"/> <button id="add">add</button>
<div id="container">
</div>
</body>
</html>


到了发布订阅模式,我也只能粗略说说,关于事件机制,还有很长的路要走。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值