DOM事件

DOM事件

Posted by limantang on November 22, 2019

DOM事件

概念明确

  • 事件流 事件在页面中传播的顺序, 反过来就是页面接收事件的顺序
  • 事件 鼠标点击, 页面加载, 鼠标悬浮, 键盘按下, 输入框输入, 聚焦等具体的操作可以被JavaScript侦测到的行为就是事件, 事件通常和函数配合使用, 当事件发生时函数才会执行
  • 事件处理程序 响应某个事件的函数就是事件处理程序, 事件处理程序名包括: onclick, onmouse, onblur等等

DOM0 DOM2 DOM3级事件处理

DOM0级

通过JS指定事件处理程序的传统方式, 就是将一个函数赋值给一个事件处理属性, 兼容性好, 至今被所有浏览器所支持

const btn = document.getElementById(`btn`);
btn.onclick = (e) => {
	console.log(e, this.id)
}

删除事件处理程序只需要将事件属性值设为null即可 btn.onclick = null 缺点: 一个事件处理程序只能对应一个处理函数

DOM2级

DOM2级事件处理方式指定了, 添加事件处理程序和函数事件处理程序的方法 addEventListener(eventName,func,isCapture) removeEventListener(eventName,func,isCapture) 参数分别是: 事件处理属性名称, 事件处理函数, 是否捕获(默认false, 不捕获) eventName不含有on 通过addEventListener添加的事件处理程序, 只能通过removeEventListener来删除, 由于这一点匿名处理函数无法删除 也因为这点, 一个事件处理程序可以添加多个事件处理函数 可以指定在哪个阶段来触发事件处理程序, 捕获和冒泡两个事件处理阶段也是在DOM2级引入的概念, 下面会详细讨论一下 兼容性方面不支持旧版本IE(个人认为不用管IE)

DOM3级

DOM3级在DOM2级的基础上规定了几种事件 UI事件, 聚焦事件, 鼠标事件, 滚轮事件, 文本事件, 键盘事件, 变动事件 DOM3是在DOM2的基础上重新定义了这些事件 DOM3还支持自定义事件, 自定义事件不是由原生DOM触发的, 是开发者通过JS触发的 可以通过createEvent('自定义事件名’) 来创建 它返回一个含有initCustomEvent方法的对象 这个方法接受四个参数 type: 字符串 指定触发的事件类型 比如 keyDown click或者是自定义的customEventName bubble: 布尔值 指定是否可以冒泡 cancelable: 布尔值 指定是否可以取消 detail: 任意值, 保存在event对象中的detail中

var  div = document.getElementById('myDiv');
var e = document.createEvent('CustomEvent');
e.initCustomEvent("myEvent",true,false,"hello world");
div.dispatchEvent(e);

因为指定了事件是冒泡的所以, 见body或者html上监听改事件的事件处理程序的函数都会被触发

beforeunload 页面卸载前触发 unload页面已经被卸载 DOMContentLoaded 页面文档完全加载并解析完毕之后触发(不包含样式CSS文件,图片, 子框架的加载) load 页面内所有元素都加载完毕(包括图片,CSS样式) readystatechange 每当文档的加载状态改变的时候就有一个readystatechange事件被触发 文档的三种状态, 通过document.readyState获取

  • loading document正在加载
  • interactive文档加载完毕, 文档已被解析, 但是例如图片,样式等子资源还在加载, DOMContentLoaded即将被触发
  • complete 文档和所有子资源加载完毕, load事件即将被触发

事件冒泡&事件捕获

出现事件冒泡和事件捕获的两种完全不同的事件流是因为历史原因, IE和Netscape两个团队不同想法, IE提出事件冒泡, Netscape提出事件捕获

事件冒泡

从事件开始的具体元素开始, 一级一级向上传播到document为止 看下面例子代码

<div id="wrapper">
    <div id=“parent”>
      <div id=“child”>child</div>
      parent
    </div>
  wrapper
</div>

当点击child元素时, 事件是这样传播的: child ==> parent ==> wrapper ==> body ==> html ==> document

事件冒泡支持性很好

事件捕获

事件捕获和事件冒泡原理恰恰相反, 它的用意是在事件到达指定的目标元素之前捕获它, 也就是具体的节点元素最后接受到事件 还是上面的代码例子 事件是这样传播的: document ==> html ==> body ==> wrapper ==> parent ==> child

支持性不好

DOM事件流

DOM2级规定的事件流包含三个阶段: 事件捕获阶段处于目标阶段事件冒泡阶段

首先发生的是事件捕获阶段, 为截获事件提供了机会, 然后就是实际的目标接受事件, 最后一个阶段就是*事件冒泡阶段

如上图所示, 假如点几个这个div,目标元素就是div这个dom 在DOM流中, 事件目标在捕获阶段不会接收到事件, 也就是说事件捕获阶段从documentbody就结束了 下一个阶段就是处于目标阶段这时事件在div元素上触发了, 并且在事件处理中被看作是事件冒泡阶段的一部分, 然后事件冒泡阶段发生, 事件又传回document 其实可以简单的理解成处于目标阶段就是冒泡阶段的一部分, 合二为一

大多数的浏览器都实现了一种特定的行为” 即使DOM2级明确规定要求捕获阶段不能涉及事件目标, 但是大多数浏览器都在捕获阶段触发目标元素上的事件, 结果就是有两个机会在目标元素上处理事件

事件处理顺序

看一段具体的代码

<!doctype html>
<html lang=“en”>
<head>
  <meta charset=“UTF-8”>
  <meta name=“viewport”
        content=“width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0”>
  <meta http-equiv=“X-UA-Compatible” content=“ie=edge”>
  <title>dom事件流</title>
  <style>
    body {
      margin: 300px;
    }
    #wrapper {
      width: 300px;
      height: 300px;
      background: aqua;
    }
    #parent {
      width: 200px;
      height: 200px;
      background: bisque;
    }
    #child {
      width: 100px;
      height: 100px;
      background: blueviolet;
    }
  </style>
</head>
<body>
  <div id=“wrapper”>
      <div id=“parent”>
        <div id="child">child</div>
        parent
      </div>
    wrapper
  </div>

  <script>
  const wrapper = document.querySelector('#wrapper');
	const parent = document.querySelector('#parent');
	const child = document.querySelector('#child');

	parent.addEventListener('click', () => {
		alert('parent-冒泡')
	}, false);
	parent.addEventListener('click', () => {
		alert('parent-捕获')
	}, true);

	// child.addEventListener('click', () => {
	// 	alert('child-冒泡')
	// }, false);
	// child.addEventListener(‘click’, () => {
	// 	alert('child-捕获’)
	// }, true);

	child.addEventListener('click', () => {
		alert('child-捕获')
	}, true);
	child.addEventListener('click', () => {
		alert('child-冒泡')
	}, false);

  </script>
</body>
</html>

这时打印的顺序是: parent-捕获 ==> child-捕获 ==> child-冒泡 ==> parent-冒泡

换个注册事件顺序

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>dom事件流</title>
  <style>
    body {
      margin: 300px;
    }
    #wrapper {
      width: 300px;
      height: 300px;
      background: aqua;
    }
    #parent {
      width: 200px;
      height: 200px;
      background: bisque;
    }
    #child {
      width: 100px;
      height: 100px;
      background: blueviolet;
    }
  </style>
</head>
<body>
  <div id=“wrapper”>
      <div id="parent">
        <div id=“child”>child</div>
        parent
      </div>
    wrapper
  </div>

  <script>
  const wrapper = document.querySelector('#wrapper');
	const parent = document.querySelector('#parent');
	const child = document.querySelector('#child');



	parent.addEventListener('click', () => {
		alert('parent-冒泡')
	}, false);
	parent.addEventListener('click', () => {
		alert('parent-捕获')
	}, true);

	child.addEventListener('click', () => {
		alert('child-冒泡')
	}, false);
	child.addEventListener('click', () => {
		alert('child-捕获')
	}, true);

	// child.addEventListener('click', () => {
	// 	alert(‘child-捕获’)
	// }, true);
	// child.addEventListener('click', () => {
	// 	alert('child-冒泡’)
	// }, false);

  </script>
</body>
</html>

这时打印的顺序是: parent-捕获 ==> child-冒泡 ==> child-捕获 ==> parent-冒泡

上面的两种情况反映出一个小结论: 在捕获阶段又在冒泡阶段调用事件处理程序时, 事件按照DOM事件流的顺序执行事件处理程序函数 当处于目标阶段时, 也就是目标元素的事件处理程序按照注册事件的书写顺序来执行

为了最大的兼容性, 最好只在冒泡阶段处理事件处理程序, 除非特殊情况

事件对象

挡在一个dom元素触发一个事件的时候, 会产生一个事件对象, 其中包含着所有和事件相关的信息 比较常用的几个信息; currentTarget 事件处理程序当前正在处理的那个元素, 也就是始终是事件监听的那个元素 始终等于this

preventDefault 取消事件默认行为, 比如a标签的跳转

stopPropagation 取消事件流传播

target 事件的目标(就是真正触发事件动作的那个元素)

<!doctype html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport"
        content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>dom事件流</title>
  <style>
    body {
      margin: 300px;
    }
    #wrapper {
      width: 300px;
      height: 300px;
      background: aqua;
    }
    #parent {
      width: 200px;
      height: 200px;
      background: bisque;
    }
    #child {
      width: 100px;
      height: 100px;
      background: blueviolet;
    }
  </style>
</head>
<body>
  <div id="wrapper">
      <div id="parent">
        <div id="child">child</div>
        parent
      </div>
    wrapper
  </div>

  <script>
  const wrapper = document.querySelector('#wrapper');
	const parent = document.querySelector('#parent');
	const child = document.querySelector('#child');

	wrapper.addEventListener('click', (e) => {

		alert('wrapper-冒泡')
	}, false);
	wrapper.addEventListener('click', function(e) {
		console.log('e.target-wrapper-捕获', e.target);
		console.log('e.currentTarget-wrapper-捕获', e.currentTarget);
		console.log('this', this);
		alert('wrapper-捕获')
	}, true);


	parent.addEventListener('click', (e) => {
		alert('parent-冒泡')
	}, false);
	parent.addEventListener('click', function(e) {
		e.stopPropagation();
		console.log('e.target-parent-捕获', e.target);
		console.log('e.currentTarget-parent-捕获', e.currentTarget);
		console.log('this', this);
		alert('parent-捕获')
	}, true);

	child.addEventListener('click', (e) => {
		alert('child-冒泡')
	}, false);
	child.addEventListener('click', (e) => {
		alert('child-捕获')
	}, true);

	// child.addEventListener('click', () => {
	// 	alert('child-捕获')
	// }, true);
	// child.addEventListener('click', () => {
	// 	alert('child-冒泡')
	// }, false);

  </script>
</body>
</html>

在parent捕获阶段e.stopPropagation()取消事件传播, 会打断接下来所有的事件 currentTargettarget的具体区别看log信息

事件委托

每个函数都是对象, 都会占用内存, 内存中对象越多, 性能越差, 事件处理程序过多的解决方式就是事件委托

事件委托就是利用事件冒泡, 只指定一个事件处理程序即可, 就可以管理一类的所有事件

源码地址 blog-source-code/index.html at master · echoheart/blog-source-code · GitHub