HTML DOM事件相关

571 阅读9分钟

什么是DOM?

JavaScript可以访问和操作存储在DOM中的内容,因此我们可以写成这个近似的等式:

API (web 或 XML 页面) = DOM + JS (脚本语言)

DOM事件级别

事件对象

Event 对象代表事件的状态,比如事件在其中发生的元素、键盘按键的状态、鼠标的位置、鼠标按钮的状态

事件是javascript和HTML交互基础, 任何文档或者浏览器窗口发生的交互, 都要通过绑定事件进行交互;

DOM级别一共可以分为四个级别:DOM0级、DOM1级、DOM2级和DOM3级。

而DOM事件分为3个级别:DOM 0级事件处理,DOM 2级事件处理和DOM 3级事件处理。由于DOM 1级中没有事件的相关内容,所以没有DOM 1级事件。

DOM0 级事件

1. 通常是直接通过 onclick 绑定到 DOM 元素上的事件。

<input onclick="xx"/>

或者

input.onclick = function(){  ...  }

第一种方式是内联模型(行内绑定),将函数名直接作为html标签中属性的属性值。

例如👉

<div onclick="btnClick()">点击</div>
<script>
function btnClick(){
    console.log("hello");
}
</script>

内联模型的缺点是不符合w3c中关于内容与行为分离的基本规范。

另外一种方式是动态绑定:

<div id="btn">点击</div>
<script>
var btn=document.getElementById("btn");
btn.onclick=function(){
    console.log("hello");
}
</script>

当希望为同一个元素/标签绑定多个同类型事件的时候(如给上面的这个btn元素绑定3个点击事件),是不被允许的。DOM0事件绑定,给元素的事件行为绑定方法,这些方法都是在当前元素事件行为的冒泡阶段(或者目标阶段)执行的。

2. 清理dom0 事件时,只需给该事件赋值为 null

  input.onclick = null

3. 同一个元素的同一种事件只能绑定一个函数,否则后面的函数会覆盖之前的函数

4.不存在兼容性问题,具有极好的跨浏览器优势,会以最快的速度绑定。

DOM2 级事件

1. dom2 事件是通过 addEventListener 绑定的事件

2.同一个元素的同种事件可以绑定多个函数,按照绑定顺序执行.

function a() {   ...   }

  function b() {   ...   }

   input.addEventListener( "click" ,a)

    input.addEventListener( "click" ,b)

3.清除 dom2 事件时,使用 removeEventListener

input.removeEventListener( "click" ,a)

dom2 级事件有三个参数

  • 第一个参数是事件名(如click);

  • 第二个参数是事件处理程序函数;

  • 第三个参数如果是true则表示在捕获阶段调用,为false表示在冒泡阶段调用。 addEventListener():可以为元素添加多个事件处理程序,触发时会按照添加顺序依次调用。 removeEventListener():不能移除匿名添加的函数。

DOM3 级事件

DOM3级事件在DOM2级事件的基础上添加了更多的事件类型,全部类型如下:

       UI事件,当用户与页面上的元素交互时触发,如:loadscroll

       焦点事件,当元素获得或失去焦点时触发,如:blurfocus

       鼠标事件,当用户通过鼠标在页面执行操作时触发如:dbclickmouseup

       滚轮事件,当使用鼠标滚轮或类似设备时触发,如:mousewheel

       文本事件,当在文档中输入文本时触发,如:textInput

        键盘事件,当用户通过键盘在页面上执行操作时触发,如:keydownkeypress

        合成事件,当为IME(输入法编辑器)输入字符时触发,如:compositionstart

        变动事件,当底层DOM结构发生变化时触发,如:DOMsubtreeModified

         同时DOM3级事件也允许使用者自定义一些事件。

注意:

1)cancelBubble可以取消事件冒泡;

2)stopPropagation可以取消事件冒泡和下沉

事件流

事件流描述的就是从页面中接收事件的顺序。而早期的IE和Netscape提出了完全相反的事件流概念,IE事件流是事件冒泡,而Netscape的事件流就是事件捕获。 通常事件冒泡是有用的,可以将事件对象的cancelBubble设置为true,即可取消冒泡。

window.onload = function(){
				
				/*
				 * 事件的冒泡(Bubble)
				 * 	- 所谓的冒泡指的就是事件的向上传导,当后代元素上的事件被触发时,其祖先元素的相同事件也会被触发
				 * 	- 在开发中大部分情况冒泡都是有用的,如果不希望发生事件冒泡可以通过事件对象来取消冒泡
				 * 
				 */
				
				//为s1绑定一个单击响应函数
				var s1 = document.getElementById("s1");
				s1.onclick = function(event){
					event = event || window.event;
					alert("我是span的单击响应函数");
					
					//取消冒泡
					//可以将事件对象的cancelBubble设置为true,即可取消冒泡
					event.cancelBubble = true;
				};
				
				//为box1绑定一个单击响应函数
				var box1 = document.getElementById("box1");
				box1.onclick = function(event){
					event = event || window.event;
					alert("我是div的单击响应函数");
					
					event.cancelBubble = true;
				};
				
				//为body绑定一个单击响应函数
				document.body.onclick = function(){
					alert("我是body的单击响应函数");
				};
				
				
			};

W3C综合了两个公司的方案,将事件的传播分成了三个阶段:

  1. 捕获阶段 在捕获阶段时从最外层的祖先元素,向目标元素进行事件的捕获,但是默认此时不会触发事件。

  2. 目标阶段 事件捕获到目标元素,捕获结束,开始在目标元素上触发事件。

  3. 冒泡阶段 事件从目标元素向他的祖先元素传递,依次触发祖先元素上的事件。

如果希望在捕获阶段就触发事件,将adEventListener()第三个参数设置为true,一般情况下默认为false

IE8及以下没有捕获阶段。

image.png

<!DOCTYPE html>
<html>
	<head>
		<meta charset="UTF-8">
		<title></title>
		<style type="text/css">
			
			#box1{
				width: 300px;
				height: 300px;
				background-color: yellowgreen;
			}
			
			#box2{
				width: 200px;
				height: 200px;
				background-color: yellow;
			}
			
			#box3{
				width: 150px;
				height: 150px;
				background-color: skyblue;
			}
			
		</style>
		
		<script type="text/javascript">
			
			window.onload = function(){
				
				/*
				 * 分别为三个div绑定单击响应函数
				 */
				var box1 = document.getElementById("box1");
				var box2 = document.getElementById("box2");
				var box3 = document.getElementById("box3");
				
					bind(box1,"click",function(){
					alert("我是box1的响应函数")
				});
				
				bind(box2,"click",function(){
					alert("我是box2的响应函数")
				});
				
				bind(box3,"click",function(){
					alert("我是box3的响应函数")
				});
				
			};
			
			
			function bind(obj , eventStr , callback){
				if(obj.addEventListener){
					//大部分浏览器兼容的方式
					obj.addEventListener(eventStr , callback , true);
				}else{
					/*
					 * this是谁由调用方式决定
					 * callback.call(obj)
					 */
					//IE8及以下
					obj.attachEvent("on"+eventStr , function(){
						//在匿名函数中调用回调函数
						callback.call(obj);
					});
				}
			}
			
		</script>
	</head>
	
	<body>
		
		<div id="box1">
			<div id="box2">
				<div id="box3"></div>
			</div>
		</div>
		
	</body>
</html>

事件委托

事件委托,通俗地讲,就是把一个元素响应事件(click、keydown......)的函数委托到另一个元素;

利用事件冒泡原理可以实现,只指定一个事件处理程序管理某一类型的所有事件。

一般来讲,会把一个或者一组元素的事件委托到它的父层或者更外层元素上,真正绑定事件的是外层元素,当事件响应到需要绑定的元素上时,会通过事件冒泡机制从而触发它的外层元素的绑定事件上,然后在外层元素上去执行函数。

1.事件委托的实现

<ul id="list">
  <li>item 1</li>
  <li>item 2</li>
  <li>item 3</li>
  ......
  <li>item n</li>
</ul>
// ...... 代表中间还有未知数个 li

下面代码实现点击li,弹出 我是事件委托触发的~ 如果采用循环遍历的子节点方法,点击一次,就要循环一次,找到所点击的li,才能执行操作。

因此,比较好的方法就是把这个点击事件绑定到他的父层,也就是 ul 上,然后在执行事件的时候再去匹配判断目标元素。

window.onload = function(){
    var oUl = document.getElementById("ul1");
   oUl.onclick = function(){
        alert("我是事件委托触发的~");
    }
}

用父级ul做事件处理,当li被点击时,由于冒泡原理,事件就会冒泡到ul上,因为ul上有点击事件,所以事件就会触发,当然,这里当点击ul的时候,也是会触发的。

但是,如果我想让事件代理的效果跟直接给节点的事件效果一样怎么办,比如说只有点击li才会触发,不怕,我们有绝招:

Event对象提供了一个属性叫target,可以返回事件的目标节点,我们成为事件源,也就是说,target就可以表示为当前的事件操作的dom,但是不是真正操作dom,当然,这个是有兼容性的,标准浏览器用ev.target,IE浏览器用event.srcElement,此时只是获取了当前节点的位置,并不知道是什么节点名称,这里我们用nodeName来获取具体是什么标签名,这个返回的是一个大写的,我们需要转成小写再做比较(习惯问题): 最佳方法

// 给父层元素绑定事件
document.getElementById('list').addEventListener('click', function (e) {
  // 兼容性处理
  var event = e || window.event;
  var target = event.target || event.srcElement;
  // 判断是否匹配目标元素
  if (target.nodeName.toLocaleLowerCase === 'li') {
    alert("我是事件委托触发的~");
    console.log('the content is: ', target.innerHTML);
  }
});

这样改下就只有点击li会触发事件了,且每次只执行一次dom操作,如果li数量很多的话,将大大减少dom的操作,优化的性能可想而知!

要想每个li被点击的效果都不一样,那么用事件委托还有用吗?

<div id="box">
        <input type="button" id="add" value="添加" />
        <input type="button" id="remove" value="删除" />
        <input type="button" id="move" value="移动" />
        <input type="button" id="select" value="选择" />
    </div>
window.onload = function(){
            var Add = document.getElementById("add");
            var Remove = document.getElementById("remove");
            var Move = document.getElementById("move");
            var Select = document.getElementById("select");
            
            Add.onclick = function(){
                alert('添加');
            };
            Remove.onclick = function(){
                alert('删除');
            };
            Move.onclick = function(){
                alert('移动');
            };
            Select.onclick = function(){
                alert('选择');
            }
            
        }

上面的实现方式至少需要4次dom操作。

事件委托方式优化

window.onload = function(){
            var oBox = document.getElementById("box");
            oBox.onclick = function (ev) {
                var ev = ev || window.event;
                var target = ev.target || ev.srcElement;
                if(target.nodeName.toLocaleLowerCase() == 'input'){
                    switch(target.id){
                        case 'add' :
                            alert('添加');
                            break;
                        case 'remove' :
                            alert('删除');
                            break;
                        case 'move' :
                            alert('移动');
                            break;
                        case 'select' :
                            alert('选择');
                            break;
                    }
                }
            }
            
        }

用事件委托就可以只用一次dom操作就能完成所有的效果,比上面的性能肯定是要好一些的。

2.事件委托的优点?

1)减少内存消耗

在JavaScript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能,因为需要不断的与dom节点进行交互,访问dom的次数越多,引起浏览器重绘与重排的次数也就越多,就会延长整个页面的交互就绪时间,这就是为什么性能优化的主要思想之一就是减少DOM操作的原因;

如果给每个列表项一一都绑定一个函数,每一个函数都是一个对象,对象越多,内存占用率就越大,自然性能就越差了。

2)动态绑定事件

在很多时候,我们需要通过 AJAX 或者用户操作动态的增加或者去除列表项元素,那么在每一次改变的时候都需要重新给新增的元素绑定事件,给即将删去的元素解绑事件;

如果用了事件委托就没有这种麻烦了,因为事件是绑定在父层的,和目标元素的增减是没有关系的,执行到目标元素是在真正响应执行事件函数的过程中去匹配的;

举个例子🤳

如果要实现这样一个效果:移入 li , li 变红,移出 li , li变白,然后点击按钮,可以向 ul 中添加一个 li 子节点。

<input type="button" name="" id="btn" value="添加" />
    <ul id="ul1">
        <li>111</li>
        <li>222</li>
        <li>333</li>
        <li>444</li>
    </ul>

采用事件委托,不需要去遍历元素的子节点,只需要给父级元素添加事件就好了,其他的都是在 js 里面的执行,这样可以大大的减少dom操作,这才是事件委托的精髓所在。

window.onload = function(){
            var oBtn = document.getElementById("btn");
            var oUl = document.getElementById("ul1");
            var aLi = oUl.getElementsByTagName('li');
            var num = 4;
            
            //事件委托,添加的子元素也有事件
            oUl.onmouseover = function(ev){
                var ev = ev || window.event;
                var target = ev.target || ev.srcElement;
                if(target.nodeName.toLowerCase() == 'li'){
                    target.style.background = "red";
                }
                
            };
            oUl.onmouseout = function(ev){
                var ev = ev || window.event;
                var target = ev.target || ev.srcElement;
                if(target.nodeName.toLowerCase() == 'li'){
                    target.style.background = "#fff";
                }
                
            };
            
            //添加新节点
            oBtn.onclick = function(){
                num++;
                var oLi = document.createElement('li');
                oLi.innerHTML = 111*num;
                oUl.appendChild(oLi);
            };
        }

3.事件委托的局限性?

适合用事件委托的事件:clickmousedownmouseupkeydownkeyupkeypress

不适合的委托的:

比如 focusblur 之类的事件本身没有事件冒泡机制,所以无法委托;

mousemovemouseout 这样的事件,虽然有事件冒泡,但是只能不断通过位置去计算定位,对性能消耗高,因此也是不适合于事件委托的;