JavaScript事件模型

601 阅读7分钟

一、事件流

事件流:从页面中接收事件的顺序。

<html>
    <body>
        <div>HELLO WORLD</div>
    </body>
</html>

事件冒泡:由微软公司提出,由最具体的元素接收事件,然后逐级向上传播到较为不具体的节点(document对象)。以上代码事件流向:div->body->html->document->window(浏览器实现包括此节点)

事件捕获:由网景公司提出,不太具体的节点(document对象)先接收事件,然后逐级向下传播到最具体的节点。以上代码事件流向:window(浏览器实现包括此节点)->document->html->body>div。

DOM事件流机制(事件传播):事件流包括事件捕获、处于目标、事件冒泡阶段。

事件捕获:window->document->html->body

处于目标:div

事件冒泡:div->body->html->document->window

这篇文章简简单单的写的挺清楚(下面引用此文标为链接1):segmentfault.com/a/119000000…

二、事件处理程序和this指向

事件处理程序:也叫事件处理器或事件监听器,响应某个事件的函数,事件处理程序以"on"开头,响应click事件的事件处理程序是onclick。

方式一:HTML事件处理程序

使用和事件处理程序同名的HTML特性来指定。

1、事件处理程序是动态创建的函数,this指向事件的目标元素,而且回调函数扩展了作用域,可以直接访问事件目标函数的属性。

回调函数执行环境的作用域链:包含事件目标元素全部属性的变量对象->包含document全部属性的变量对象->回调函数变量对象->全局变量对象,如下所示。因此,在回调函数中直接使用事件目标元素的属性值。

function(){
    with(document){
        with(this){
            console.log(value);  //元素属性值
        }
    }
}

2、事件处理程序是全局作用域定义的函数。this指向的是window对象,要想获得事件目标对象,需要使用event.target。(可以认为回调时,这个函数已经不是由事件目标对象调用的)

<body>
    <!--回调函数是动态创建函数-->
    <input type="button" id="ele" value="点击ele" onclick="console.log('ele',value,this)"></input>
    <!--回调函数是全局作用域定义的函数-->
    <input type="button" id="ele0" value="点击ele0" onclick="handleClick(event)"></input>
    <script>
        function handleClick(event){
            console.log("ele0 ",this)  //输出window对象
            console.log("ele0 ",event.target.value,event.target);    //输出input元素
        }
    </script>
</body>

这种方式绑定的事件处理程序,事件处理程序在事件冒泡阶段处理事件。修改链接1的HTML代码如下,点击页面中心浅蓝色的部分时,先是弹出123,接着弹出456,最后弹出789。

<div id="wrap" onclick="alert('789')">
	<div id="outer" onclick="alert('456')">
	  <div id="inner" onclick="alert('123')"></div>
	</div>
</div>

方式二:DOM0级事件处理程序

将回调函数赋值为元素的事件处理程序属性。事件处理程序在事件冒泡阶段处理事件。在链接1的HTML和CSS的基础上,修改JS如下。点击页面中心浅蓝色的部分时,先是弹出123,接着弹出456,最后弹出789。

var wrap = document.getElementById('wrap');
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');

wrap.onclick=function(){
	alert('789');
};
outer.onclick=function(){
	alert('456');
};
inner.onclick=function(){
	alert('123');
};

方式三:DOM2级事件处理程序

addEventListener函数,入参是事件名、事件处理回调函数、事件捕获模式表示(true为捕获,false为冒泡(默认))。可根据设置,在冒泡阶段或者捕获阶段处理事件。

如下所示,方式二和方式三这两种方式绑定的事件处理程序都不能使用箭头函数,否则this指向不是指向事件目标对象。(箭头函数没有this,沿着作用域链找到的是全局执行环境作用域的this)

<body>
    <div>
        <input type="button" id="ele1" value="点击ele1"></input>
        <input type="button" id="ele2" value="点击ele2"></input>
        <input type="button" id="ele3" value="点击ele3"></input>
        <input type="button" id="ele4" value="点击ele4"></input>
    </div>
    <script>
        //DOM0级事件处理程序
        var ele1=document.getElementById("ele1")
        ele1.onclick=function(){
            console.log("ele1 ",this);  //输出input元素
        }
        var ele2=document.getElementById("ele2")
        ele2.onclick=()=>{
            console.log("ele2 ",this);  //window
        }
	//DOM2级事件处理程序	
        var ele3=document.getElementById("ele3")
        ele3.addEventListener("click",function(){
            console.log("ele3 ",this);  //输出input元素
        },false)
        var ele4=document.getElementById("ele4")
        ele4.addEventListener("click",()=>{
            console.log("ele4 ",this);   //输出window对象
        },false)
    </script>
</body>

以上优缺点以及应该使用哪种方式:

MDN官方文档:developer.mozilla.org/zh-CN/docs/…

HTML事件处理程序:不推荐使用,HTML和JavaScript耦合度太高。另外两种可以随意使用。

DOM0级事件处理程序:兼容性好,向下兼容到IE8,监听器可移除。但多次给事件目标对象的onclick属性赋值会覆盖之前的属性值,不能绑定多个监听同一事件的事件处理程序。

DOM2级事件处理程序:只向下兼容到IE9,监听器可移除,且事件目标对象可以绑定多个监听同一事件的事件处理程序,按添加顺序触发。

方式四:IE事件处理程序

只是IE浏览器支持,如果页入需要支持IE8以下更低版本做兼容,才需要用这种方式。文档的头部添加上<meta http-equiv="X-UA-Compatible" content="IE=10">,并在IE浏览器运行。this的指向是指向window对象。事件目标对象可以绑定多个监听同一事件的事件处理程序,按添加顺序相反的顺序触发。

<body>    
	<div>        
	    <input type="button" id="ele" value="点击ele"></input>    
	</div>    
	<script>        
	    var ele=document.getElementById("ele")        
	    ele.attachEvent("onclick",function(){
	        console.log("ele",this);  //输出window对象        
	    })    
	</script>
</body>

跨浏览器的事件处理程序

结合以上事件处理程序的方式,兼容各种浏览器及IE低版本的代码:

移除事件处理程序

为什么最佳实践要移除实践处理程序?因为事件监听器回调的引用有可能造成内存泄漏,而且再IE旧版本中还可能存在循环引用引起内存泄漏,甚至乎事件监听器自身也可能被泄漏(juejin.cn/post/699800…)。再者,事件处理程序本身就占用着内存,如果不需要最好移除以腾出空间。另一方面,给元素指定事件处理程序,运行的浏览器代码与支持页面交互的JavaScript代码就建立多一个链接,链接越多,页面执行起来就越慢。

移除时机:

1、在必要时确定事件处理程序不用就移除

<div id="myDiv">        
	<input type="button" value="Click me" id="myBtn"/>    
</div>    
<script>        
	var btn=document.getElementById("myBtn");        
        btn.onclick=function(){            
		console.log("clicked")    
                btn.onclick=null;  //在btn元素要被覆盖之前移除事件处理程序        
		document.getElementById("myDiv").innerHTML="Processing..." ; 
	}
</script>

2、在页面卸载时移除

移除方法:

  • DOM0级事件处理程序:btn.onclick=null
  • DOM2级事件处理程序:btn.removeEventListener()函数
  • IE事件处理程序:btn.detachEvent()函数

三、事件对象

无论绑定事件处理程序是什么方式,浏览器都会将事件对象event传递进事件处理程序中。event对象在事件处理程序执行阶段存在,事件执行完毕则销毁。IE事件处理程序比较特殊(attachEvent添加),事件对象不是作为参数传入事件处理程序,而是通过window.event访问。

1、event.currentTaget和event.target的区别

在链接1的HTML和CSS的基础上,修改JS如下,点击页面中心浅蓝色的部分(inner元素)

var wrap = document.getElementById('wrap');
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');

wrap.onclick=function(event){
	console.log('789',event.target,event.currentTarget);
};
outer.onclick=function(event){
	console.log('456',event.target,event.currentTarget);
};
inner.onclick=function(event){
	console.log('123',event.target,event.currentTarget);
};

输出是123, inner元素 ,inner元素;456, inner元素 ,outer元素;789, inner元素 ,wrap元素;

target:事件的目标元素;

currentTarget:其事件处理程序当前正在在处理事件的元素;在IE事件对象中,event.srcElement表示事件目标对象。

上面的例子中,inner是被点击的元素,也就是事件的目标元素。inner的外层元素outer和wrap也绑定onclick事件处理程序,所以也会处理点击事件。随着事件冒泡,当前正在处理点击事件的元素一直变化,所以currentTarget是变化的,而target始终是那个被点击的事件目标元素。

2、event.preventDefault():阻止特定事件的默认行为

常用于链接,链接被点击时会导航到指定href,如果想要阻止这个行为,可以在点击事件的事件处理程序中调用event.preventDefault()方法。

在IE事件对象中,event.returnValue=false阻止特定事件的默认行为。

3、event.stopPropagation():取消事件进一步捕获或冒泡。如果内层元素的事件处理程序中调用了此函数,则事件不会传播到外层元素中。

var wrap = document.getElementById('wrap');
var outer = document.getElementById('outer');
var inner = document.getElementById('inner');

wrap.onclick=function(event){
	console.log('789');
};
outer.onclick=function(event){
	console.log('456');
};
inner.onclick=function(event){
	console.log('123');
	event.stopPropagation();
};

在上面代码中,点击inner元素,但inner元素的事件处理程序中调用了event.stopPropagation()函数,所以,指挥打印输出123。

在IE事件对象中,event.cancelBubble=true,取消事件进一步捕获或冒泡。

四、事件委托

事件委托:利用事件冒泡机制,针对事件处理程序过多问题的解决方案。

响应事件和绑定事件的元素不一样,一般,绑定事件的元素是响应事件元素的父元素。这需要根据页面解构和响应事件要做的事,来确定怎么找到要响应事件的元素,这也是最关键的。

参考链接:juejin.cn/post/684490…,下面的例子是参考链接改写的。

<ul id="list">
	<li><span>item 1</span></li>
	<li>item 2</li>
	<li>item 3</li>
</ul>
<script>
	/*
	li元素响应事件,事件触发时打印li元素内容。
	利用事件委托机制,可给li父层元素ul绑定事件。
	但如果li元素内部还有元素,事件委托就不那么容易写了。因为event.target和要绑定的元素不一样。
	*/
	document.getElementById('list').addEventListener('click', function (e) {
		// 兼容性处理
		var event = e || window.event;
		var ele = event.target || event.srcElement;  //响应事件元素,预期是li
		while(!ele.matches("li")){
			ele=ele.parentNode;  //如果不是li,则查找其父节点是否是li
		}
		if (ele.matches("li")) {
			console.log('the content is: ',ele.innerHTML);
		}
	});
</script>

事件委托最大的优势减少了定义事件监听器的内存消耗,同时使得运行的浏览器代码与支持页面交互的JavaScript代码建立的链接更少,加快页面运行速度。