前言
JavaScript与html之间的交互通过事件
实现,它代表浏览器窗口或者是文档的某个特殊的时刻,可以使用事件发生时的处理程序来订阅事件,这也就是观察者模式
,它能够做到行为和展示的分离
一、事件流
说明: 事件流描述了页面接收事件的顺序
1.事件冒泡
说明: IE的事件流被称为事件冒泡
,其原因是事件在定义时会从最具体的元素开始触发,然后一直向上传播到文档节点
html
<!DOCTYPE html>
<html>
<body>
<div id="myDiv">Click Me</div>
</body>
</html>
当
div
上面的click
事件触发后,会按照顺序,在body
、html
、document
上都会触发这个事件
2.事件捕获
说明: 这也是一种事件流,只不过事件触发的顺序与事件冒泡相反
,它会从最不具体的元素开始触发,也就是document
,最具体的元素最后触发,在上面的例子也就是div
,这样做的目的是为了在事件到达最终目标前拦截事件
如果以上面例子为例,其触发顺序:
document
、html
、body
、div
3.DOM事件流
说明: 规范中规定事件流存在三个阶段:事件捕获
、到达目标
、事件冒泡
,事件捕获最先开始为了提前拦截事件提供可能,响应事件最迟
在冒泡阶段完成,一般认为到达目标存在于冒泡阶段
对于上面的例子,捕获阶段是
document -> html -> body
,到达目标是body -> div
,冒泡则是div -> body -> html -> document
二、事件处理程序
说明: 事件意味着用户或者浏览器执行了某个动作,比如上面的click
点击事件,在事件结束会调用一个函数,这个函数称为事件处理程序
,其名字以on
开头,那么click
事件对应的处理程序则是onclick
1.HTML事件处理程序
说明: 在html元素上面绑定事件可以以属性
的方式进行绑定,不过属性的值必须是能够执行的JavaScript代码
,那也就不能使用未经转义的html的字符,关于属性的值,可以直接将逻辑写在里面,也可以将逻辑封装成一个函数,再调用这个函数
html
<!-- 直接写逻辑 -->
<input type="button" value="Click Me" onclick="console.log('Clicked')"/>
html
<!-- 将逻辑进行封装 -->
<script>
function showMessage() {
console.log("Hello world!");
}
</script>
<input type="button" value="Click Me" onclick="showMessage()" />
this:
全局作用域下的具名函数,this指向window
2.DOM0事件处理程序
说明: 传统方式是把一个函数赋值给DOM元素的一个事件处理程序属性,当然,可以将事件处理程序属性设置为null
,从而移除添加的处理程序
js
let btn = document.getElementById("myBtn");
btn.onclick = function() {
console.log("Clicked");
};
this:
这样所赋函数被视为元素的方法。因此,事件处理程序会在元素的作用域中运行,this指向触发该事件的元素
3.DOM2事件处理程序
说明: 主要通过addEventListener(事件名,处理函数,函数是否在捕获阶段调用)
和removeEventListener(事件名,处理函数,函数是否在捕获阶段调用)
来完成,它们的第三个参数默认值是false
,也就是默认在事件冒泡的阶段执行处理函数,对于removeEventListener
想要移除添加的处理程序,需要传入与addEventListener
相同的参数才可以,这也就是说添加的匿名函数无法移除
js
// 添加一个处理程序
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
js
// 添加多个处理程序(按执行顺序触发)
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
btn.addEventListener("click", () => {
console.log("Hello world!");
}, false);
js
// 无法移除
let btn = document.getElementById("myBtn");
btn.addEventListener("click", () => {
console.log(this.id);
}, false);
btn.removeEventListener("click", function() {
console.log(this.id);
}, false);
js
// 可以移除
let btn = document.getElementById("myBtn");
let handler = function() {
console.log(this.id);
};
btn.addEventListener("click", handler, false);
btn.removeEventListener("click", handler, false);
this:
使用addEventListener
添加事件,this指向事件左边的节点
4.IE事件处理程序
说明: 它也是使用的两个方法,attachEvent()
和detachEvent()
,与上面的是类似的,只不过存在下面的区别:
- IE的两个方法的第一个参数是需要加上
on
的- IE的两个方法的函数中的
this
指向window
- IE的方法如果添加多个事件其
触发的顺序反向
- IE的方法在处理事件的阶段都在事件
冒泡阶段
5.兼容事件处理程序
说明: 这个可以选择JavaScript完成,也可以自己进行封装,必须下面这样简单实现
js
let EventUtil = {
/**
* @description 添加事件
* @param element 元素节点
* @param type 事件类型
* @param handler 事件处理函数
*/
addHandler: function(element, type, handler) {
if (element.addEventListener) {
element.addEventListener(type, handler, false);
} else if (element.attachEvent) {
element.attachEvent("on" + type, handler);
} else {
element["on" + type] = handler;
}
},
/**
* @description 移除事件
* @param element 元素节点
* @param type 事件类型
* @param handler 事件处理函数
*/
removeHandler: function(element, type, handler) {
if (element.removeEventListener) {
element.removeEventListener(type, handler, false);
} else if (element.detachEvent) {
element.detachEvent("on" + type, handler);
} else {
element["on" + type] = null;
}
}
};
三、事件对象
说明: 在 DOM中发生事件时,所有相关信息都会被收集并存储在一个名为event
的对象中,有时候在使用的时候将其简写为e
,同时event
对象是传给事件处理程序的唯一
参数,并且不同的事件的事件对象存在不同的属性和方法,但是它们存在公共的(只读)
公共的属性和方法:
bubbles(布尔值):
表示事件是否冒泡cancelable(布尔值):
表示是否可以取消事件的默认行为currentTarget(元素):
当前事件处理程序所在的元素defaultPrevented(布尔值):
true 表示已经调用preventDefault()方法detail(整数):
事件相关的其他信息eventPhase(整数):
表示调用事件处理程序的阶段:1 代表捕获阶段,2 代表到达目标,3 代表冒泡阶段preventDefault()(函数):
用于取消事件的默认行为。只有 cancelable 为 true 才可以调用这个方法stopImmediatePropagation()(函数):
用于取消所有后续事件捕获或事件冒泡,并阻止调用任何后续事件处理程序stopPropagation()(函数):
用于取消所有后续事件捕获或事件冒泡。只有 bubbles为 true 才可以调用这个方法target(元素):
事件目标,也就是需要操作的元素trusted(布尔值):
true 表示事件是由浏览器生成的。false 表示事件是开 发者通过 JavaScript 创建的type(字符串):
被触发的事件类型View(AbstractView):
与事件相关的抽象视图。等于事件所发生的 window 对象
1.DOM事件对象
注意: event对象
只在事件处理程序执行期间存在,一旦执行完毕,就会被销毁。
html
<input type="button" value="Click Me" onclick="console.log(event.type)">
js
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log(event.type); // "click"
};
btn.addEventListener("click", (event) => {
console.log(event.type); // "click"
}, false);
如果事件处理函数直接挂载在操作的节点上面,那么
this
、currentTarget
和target
是相等的
html
<!-- 比如我操作按钮,然后给按钮绑定事件 -->
<button>点击</button>
<script>
let btn = document.getElementsByTagName("button")[0];
btn.addEventListener("click", function (e) {
console.log(e.currentTarget === this); // true
console.log(e.target === this); // true
});
</script>
preventDefault()
方法可以阻止默认行为
js
// 链接的默认行为就是在被单击时导航到 href 属性指定的 URL,
// 阻止后就不会跳转了
let link = document.getElementById("myLink");
link.onclick = function(event) {
event.preventDefault();
};
stopPropagation()
方法一般用于阻止事件冒泡
js
let btn = document.getElementById("myBtn");
btn.onclick = function(event) {
console.log("Clicked");
event.stopPropagation();
};
// 这个click事件并不会触发,因为冒泡的行为取消掉了
document.body.onclick = function(event) {
console.log("Body clicked");
};
2.IE事件对象
说明: IE的event对象
会因为事件注册的方式不同而导致访问的形式不一样,如果 事件处理程序是使用DOM0方式
指定的,则event对象只是window对象的一个属性
,如果事件处理程序是使用attachEvent()指定
的,则 event对象会作为唯一的参数
传给处理函数,但其还是window的属性
,如果是使用HTML属性
方式指定的事件处理程序,则event对象同样可以通过变量event
访问,当然它也会存在不同的事件其事件对象不一样,但是也存在相同的属性和方法
相同的属性和方法:
cancelBubble(布尔值):
默认为 false,设置为 true 可以取消冒泡returnValue(布尔值):
默认为 true,设置为 false 可以取消事件默认行为srcElement(元素):
它是只读
的,表示事件目标,这里可以用于替换this
type(字符串):
它是只读
的,表示触发的事件类型
js
var btn = document.getElementById("myBtn");
btn.onclick = function() {
let event = window.event;
console.log(event.type); // "click"
};
js
var btn = document.getElementById("myBtn");
btn.attachEvent("onclick", function(event) {
console.log(event.type); // "click"
});
html
<input type="button" value="Click Me" onclick="console.log(event.type)" />
3.兼容事件对象
说明: 虽然 DOM 和 IE 的事件对象并不相同,但它们有足够的相似性可以实现跨浏览器方案,所以可以简单处理下
js
var EventUtil = {
getEvent: function(event) {
return event ? event : window.event;
},
getTarget: function(event) {
return event.target || event.srcElement;
},
preventDefault: function(event) {
if (event.preventDefault) {
event.preventDefault();
} else {
event.returnValue = false;
}
},
stopPropagation: function(event) {
if (event.stopPropagation) {
event.stopPropagation();
} else {
event.cancelBubble = true;
}
}
};
四、事件类型
1.用户界面事件
说明: 它们是涉及与BOM交互的通用浏览器事件,主要有以下几个:
load:
在 window 上当页面加载完成后触发,在窗套都加载完成后触发,在<img>元素上当图片加载完成后触发,在<object>元素上当相应对象加载完成后触发。unload:
在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在<object>元素上当相应对象卸载完成后触发。abort:
在<object>元素上当相应对象加载完成前被用户提前终止下载时触发。error:
在 window 上当 JavaScript 报错时触发,在<img>元素上当无法加载指定图片时触发, 在<object>元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时 触发。select:
在文本框(<input>或 textarea)上当用户选择了一个或多个字符时触发。resize:
在 window 或窗格上当窗口或窗格被缩放时触发。scroll:
当用户滚动包含滚动条的元素时在元素上触发。<body>元素包含已加载页面的滚动条。
(1)load
触发时刻: 在window
上当页面加载完成后触发,在窗套(<frameset>
)上当所有窗格(<frame>
) 都加载完成后触发,在<img>
元素上当图片加载完成后触发,在<object>
元素上当相应对象加载完成后触发
js
// 使用js的方式添加load事件
window.addEventListener("load", (event) => {
console.log(event);
});
根据规定,load事件应该在document而非window上触发。可是为了
向后兼容
,所有浏览器都在window上实现了load事件。
html
<!DOCTYPE html>
<html>
<head>
<title>向body元素添加onload属性</title>
</head>
<body onload="console.log(event)"></body>
</html>
由于
html
上没有window
属性,那么任何在window
上发生的事件,都可以通过给元素上对应的属性
赋值来指定,这是一种向后兼容的策略,其次,如果html标签上面存在onload
这个属性都可以这样添加事件,不过需要注意下<img>
这个标签,当它被设置src
属性的时候就会开始下载,<script>
这个标签会在JavaScript文件加载完成
后触发load事件
,如果需要下载文件,则同时
需要src
属性被设置以及<script>
这个元素被添加到页面中去
(2)unload
触发时刻: 在window
上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在<object>
元素上当相应对象卸载完成后触发。
说明: 这个事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用
,以避免内存泄漏
js
window.addEventListener("unload", (event) => {
console.log(event);
});
html
<!DOCTYPE html>
<html>
<body onunload="console.log(event)"></body>
</html>
无论使用何种方式,都要
注意
事件处理程序中的代码,避免报错
(3)resize
触发时刻: 当浏览器窗口被缩放到新高度或宽度
说明: 它会在window
上面触发,那么添加事件也有两种方法(跟上面一样),不过优先使用JavaScript的方式添加,其次这个事件会随着用户缩放浏览器窗口不断触发
,那也就应该避免在这个事件处理程序中执行过多计算。否则可能由于执行过于频繁而导致浏览器响应明显变慢。
js
window.addEventListener("resize", (event) => {
console.log("Resized");
});
(4)scroll
触发时刻: 用户滚动包含滚动条的元素时在元素在元素上面触发,通常指用户滚动页面
说明: 虽然scroll
事件发生在window
上,但实际上反映的是页面中相应元素的变化。
js
window.addEventListener('scroll', function() {
// 在这里编写处理scroll事件的代码
});
2.焦点事件
focus:
当元素获取
焦点的时候触发,它被所有浏览器支持但是这个事件不冒泡blur:
当元素失去
焦点的时候触发,它被所有浏览器支持但是这个事件不冒泡
3.鼠标和滚轮事件
说明: 其主要作用是定位设备,使用的事件如下:
click:
在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。这主要是基于无障碍的考 虑,让键盘和鼠标都可以触发 onclick 事件处理程序。dblclick:
在用户双击鼠标主键(通常是左键)时触发。mousedown:
在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。mouseenter:
在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡
,也不会在光标经过后代元素时触发。mouseleave:
在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡
,也不会在光标经过后代元素时触发。mousemove:
在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发。mouseout:
在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元 素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。mouseover:
在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。mouseup:
在用户释放鼠标键时触发。这个事件不能通过键盘触发。
注意: click和dblclick之间触发的事件有:mousedown -> mouseup -> click -> mousedown -> mouseup -> click -> dblclick
,这是一个过程,如果前面的事件少触发一个,那么后面的事件就不会触发
(1)客户端坐标
说明: 客户端坐标
是事件发生时鼠标光标在客户端视口
中的坐标,说简单点就是将当前元素距离左侧和上边的距离写成坐标(x,y)的形式,这个坐标就是客户端坐标(如下图),x值和y值可以通过event.clientX
和event.clientY
来获取
将页面中滚动条和顶部工具栏去掉,剩下的地方就是
视口
了
注意,
这两个值跟页面有没有滚动是没有关系的
,比如当前元素的位置的event.clientX的值是100,我把滚动条下拉1000px的距离,event.clientX的值也还是100
(2)页面坐标
说明: 页面坐标是事件发生时鼠标光标在页面上的实际坐标,也就是说这个坐标的值会包含滚动条滚动的距离,页面坐标(x,y)
分别使用(event.pageX,event.pageY)
来获取,至于滚动条滚动的距离,使用scrollLeft
和scrollTop
这两个属性获取,如果是混杂模式
,属性存在document.body
上,如果是标准模式
,属性存在于document.documentElement
上
页面没有滚动:
pageX = clientX、clientY = pageY
页面存在滚动:
pageX / pageY = clientX + scrollLeft / scrolTop
(3)屏幕坐标
说明: 这个就是指当前元素在屏幕上实际的位置,那根据意思,这个坐标(x,y)的值与滚动条是否滚动就没有关系了,这两个值分别使用event.screenX
和event.screenY
来获取
(4)修饰键
说明: 修饰键存在四个:shift
、ctrl
、window
、alt
,这四个按键分别对应event
对象的四个属性shiftKey
、ctrlKey
、metaKey
、altKey
,它们的属性值都是布尔值,表示这个键是否被按下
(5)相关元素
说明: 对 mouseover和 mouseout 事件而言,还存在与事件相关的其他元素。这两个事件都涉及从一个 元素的边界之内把光标移到另一个元素的边界之内。对 mouseover 事件来说,事件的主要目标是获得 光标的元素,相关元素是失去光标的元素。类似地,对 mouseout 事件来说,事件的主要目标是失去光 标的元素,而相关元素是获得光标的元素,获取相关元素的信息可以使用event.target
属性获取,这个属性只有
事件的取值是这两个的时候才会存在值,其它时间都是null
(6)鼠标按键
说明: 对于mousedown
和mouseup
两个事件,其返回的event
对象中存在一个button
属性,表示你按的是鼠标的哪一个键,0
表示鼠标左键,1
表示鼠标滚轮,2
表示鼠标右键
(7)额外事件信息
说明: event
对象上面存在一个detail
属性,对于鼠标事件来说,它的值是一个数字,表示在同一个像素上面发生了多少次一次mousedown
紧跟一次mouseup
的行为,它的初始值是0
,如果连续发生这样的行为,他它就会累加,如果鼠标在 mousedown
和mouseup
之间移动了,则detail
会重置为0
。
4.键盘与输入事件
说明: 键盘事件是用户操作键盘才触发的,它包含两个事件keydown
和keyup
,分别在按下触发(按住不松开会持续触发)
和松开触发
(1)键码
说明: 对于keydown
和keyup
事件,当按下一个键盘的按键,其event
对象中的keyCode(已废弃)
属性会保存对应按键的ASCII码
,具体的值可以参考菜鸟教程
五、性能
说明: 在 JavaScript 中,页面中事件处理程序的数量与页面整体性能直接相关。首先
,每个函数都是对象,都占用内存空间,对象越多,性能越差。其次
,为指定事件处理 程序所需访问 DOM 的次数会先期造成整个页面交互的延迟。
1.事件委托
说明: 这个的原理
是使用的事件冒泡。它可以使用一个事件处理程序来处理一类事件,以此解决过多事件处理程序的问题,由于事件会冒泡到document
上,那也就可以用一个click事件
绑定页面的多个或者所有元素,看下面的例子:
html
<!-- 给每个li都绑定click事件 -->
<ul id="myLinks">
<li id="goSomewhere">Go somewhere</li>
<li id="doSomething">Do something</li>
<li id="sayHi">Say hi</li>
</ul>
js
// 不用事件委托处理
let item1 = document.getElementById("goSomewhere");
let item2 = document.getElementById("doSomething");
let item3 = document.getElementById("sayHi");
// 会发现存在很多重复的事件处理程序
item1.addEventListener("click", (event) => {
location.href = "http:// www.wrox.com";
});
item2.addEventListener("click", (event) => {
document.title = "I changed the document's title";
});
item3.addEventListener("click", (event) => {
console.log("hi");
});
js
// 使用事件委托处理
// 这里只需要一个处理程序就可以,然后使用event的id属性进行区别处理
let list = document.getElementById("myLinks");
list.addEventListener("click", (event) => {
let target = event.target;
switch(target.id) {
case "doSomething":
document.title = "I changed the document's title";
break;
case "goSomewhere":
location.href = "http:// www.wrox.com";
break;
case "sayHi":
console.log("hi");
break;
}
});
事件委托的优点:
- document对象随时可用,任何时候都可以给它添加事件处理程序,这意味着只要页面渲染出可点击的元素,就可以无延迟地起作用。
- 节省花在设置页面事件处理程序上的时间。只指定一个事件处理程序既可以节省 DOM 引用,也 可以节省时间。
- 减少整个页面所需的内存,提升整体性能。
2.删除事件处理程序
说明: 把事件处理程序指定元素后,浏览器代码会与负责交互的代码产生联系,联系越多,性能越差,所以删除无用的事件处理程序也能提升性能,很多 Web 应用性能不佳都是由于无用的事件处理程序长驻内存导致的,导致的原因有两个,一个是删除带有事件处理程序的元素
,虽然元素被删除,但是事件处理程序无法被垃圾回收,存放在内存中,久而久之就会影响性能,二是页面卸载
,影响的原因也是一样
html
<div id="myDiv">
<button id="myBtn">点击</button>
</div>
<script type="text/javascript">
let btn = document.getElementById("myBtn");
btn.onclick = function () {
// btn.onclick = null; // 删除事件处理程序
// 执行操作,虽然节点删除掉,但是事件处理程序依然存在
document.getElementById("myDiv").innerHTML = "Processing...";
};
</script>
在事件处理程序中删除节点也会阻止事件冒泡
,只有目标节点仍然存在于文档中,事件冒泡才会继续