dom事件
术语
-
事件:发生一件事
-
事件类型:发生什么事情;点击、鼠标按下、鼠标抬起、鼠标移入、鼠标移出、键盘按下、键盘抬起...
-
事件处理程序:一个函数,当某件事情发生时运行。
-
事件注册:将一个事件处理程序,挂载到某个事件上。
事件流
事件流:当某个事件发生的时候,哪些元素会监听到该事件发生,这些元素发生该事件的顺序。
事件冒泡:先触发最里层的元素,然后再依次触发外层元素 事件捕获:先触发外层的元素,然后再依次触发里面元素 上面的外层里层可以理解为父,祖先元素,子元素。 当一个元素发生了某个事件时,那该元素的所有祖先元素都发生了该事件
事件冒泡是由IE提出的,而事件捕获则是由Netscape(网景)提出的事件流概念。
后来ECMAScript将两种模型进行了整合,制定了统一的标准:先捕获再冒泡

现在整合后的标准事件流就有了三个阶段:
- 事件捕获阶段(目标在捕获阶段不接收事件)
- 目标阶段 (事件的执行阶段,此阶段会被归入冒泡阶段)
- 事件冒泡阶段 (事件传回Dom根节点)
Tips: DOM2级事件规定了在捕获阶段不会涉及到目标阶段事件,但在IE9、Safari、Chrome、Firefox和Opera9.5及更高版本都会在捕获阶段触发目标事件上的事件
目前,标准规定,默认情况下(现在的默认情况是在冒泡触发,但并不是代表没有事件的处理没有捕获阶段,这是两个概念),事件是冒泡的方式触发。 上面的这句话和前面的事件流是先捕获后冒泡不冲突。
事件源、事件目标:事件目标阶段的元素
事件注册
事件绑定
dom0
将事件名称前面加上on,作为dom的属性名,给该属性赋值为一个函数,即为事件注册。
移除:重新给事件属性赋值,通常赋值为null和undefined
dom2
dom对象.addEventListener:注册事件
与dom0的区别
- dom2可以为某个元素的同一个事件,添加多个处理程序,按照注册的先后顺序运行
<script>
var btn = document.getElementById("btn1");
btn.addEventListener("click", function(){
console.log("1");
})
btn.addEventListener("click", function(){
console.log("2");
})
</script>
addEventListener 为同一个元素添加多个事件,依次执行。
- dom2允许开发者控制事件处理的阶段,使用第三个参数,表示是否在捕获阶段触发,传true 表示在捕获阶段触发,传false表示在冒泡阶段触发。默认在冒泡阶段触发。
- 如果元素是目标元素(事件源),第三个参数无效,啥是目标元素呢?目标元素就是你最终点击或者进行其他动作的元素。如果目标元素第三个参数都是传一样的值,那么还是按照注册先后执行,如果一个是传false,一个是传true,那么在传true的先执行。
- 第三个参数有两种写法,可以直接传一个值就是 true或者false,表示是否在捕获阶段触发事件,还有可以传一个配置对象。配置对象很明显就是为了传多个值,这个配置对象就可以传两个值,一个表示是否在捕获阶段触发,一个是事件是否就执行一次。
<script>
var btn = document.getElementById("btn1");
btn.addEventListener("click", function(){
console.log("1");
})
btn.addEventListener("click", function(){
console.log("2");
},true)
</script>
上面的代码输出结果就是 2,1。因为前面的一个事件的注册是在默认阶段,也就是冒泡阶段,而后面的一个事件是在捕获阶段,可以看下上面的图,事件都是先捕获而后冒泡的。
第三个参数写成对象的示例
div.addEventListener("click", divHandler1, {
capture: true, //是不是在冒泡阶段捕获的
once:true,//是不是在就执行一次
})
事件的移除:dom对象.removeEventListener(事件名, 处理函数);
dom2中如果要移除事件,不能使用匿名函数 为什么不能使用匿名函数呢? 如果你使用匿名函数添加的事件,后面你在移除这个方法的时候,你无法拿到事件对象,然后你就无法移除。
btn.addEventListener("click", function(){
console.log("这是一个匿名函数");
});
btn.removeEventListener("click",function(){
console.log("这是一个匿名函数");
});
上面看着函数是同一个,其实不是,是因为函数本身也是一个对象,两个步骤是两个函数对象,所以后者并不能将前面的那个移除掉。
正确的做法是:将函数的对象用一个参数引用着
function handler1() {
console.log("btn1");
}
btn.addEventListener("click", handler1, {
once: true
});
btn.removeEventListener("click", handler1);
细节:
- dom2在IE8及以下不兼容,需要使用attachEvent,detachEvent添加和移除事件
- 添加和移除事件时,可以将第三个参数写为一个对象,进行相关配置
事件对象
事件对象封装了事件的相关信息
获取事件对象
- 通过事件处理函数的参数获取
- 旧版本的IE浏览器通过window.event获取
获取事件处理函数的参数的方式
var a = document.querySelector("a");
var btn = document.querySelector("button");
a.addEventListener("click", function(e) {
// e.preventDefault(); //阻止浏览器的默认行为
console.log("被点击了!");
})
btn.onclick = function(e){
// 下面三行代码,任何一行都可以阻止浏览器的默认行为。
return false;
// e.preventDefault();
// e.returnValue = false;
}
事件对象的通用成员(包括本章点的所有事件 鼠标事件,键盘事件或者其他等,上面的代码就是方法里面的e)
- target & srcElement 正常使用target就够了,这里用srcElement是为解决兼容性。
事件目标(事件源)可以理解为iOS里面highTest寻找最合适的view,但是这里寻找target的过程和寻找最佳view 的过程有区别的
事件委托:通过给祖先元素注册事件,在程序处理程序中判断事件源进行不同的处理。
通常,事件委托用于动态生成元素的区域。 还有一种情况是区域内的子元素太多了,添加或者删除事件都太麻烦了。
-
currentTarget 当前目标:获取绑定事件的元素,等效于this,可以看做上面onclick 和 addEventListener的调用者。而不是上面的target.举一个例子理解一下,比方说div里面有很多几个button,对div注册点击事件,此时点击了button,target就是button,而currentTarget就是当前的点击事件注册对象div。
-
type
字符串,得到事件的类型
- preventDefault & returnValue
preventDefault方法
阻止浏览器默认行为。可以理解阻止浏览器的正常的行为,比方说本该输入文字的,阻止后就不输入了。默认行为是一个统称。
<a href="https://baidu.com">百度</a>
var a = document.querySelector("a");
a.addEventListener("click", function(e) {
e.preventDefault(); //阻止浏览器的默认行为
console.log("被点击了!");
})
a标签默认行为点击会跳转,这里调用e.preventDefault()之后a标签就不会再有默认行为跳转了。
<form action="https://www.taobao.com">
<p>
<input type="text" name="account">
</p>
<p>
<button>提交</button>
</p>
</form>
var btn = document.querySelector("button");
btn.onclick = function(e){
return false;
// e.preventDefault();
// e.returnValue = false;
}
阻止form标签的默认行为。
dom0的方式:在事件处理程序中返回false
针对a元素,可以设置为功能性链接解决跳转问题。跳转对a元素来说就是默认行为。
- stopPropagation方法
阻止事件冒泡,propagation的意思就是传播的意思,stopProgagatition就是阻止冒泡,阻止事件的传播。
- eventPhase
得到事件所处的阶段,phase的意思是阶段,连起来就是事件所处的阶段。这个属性是一个枚举值。捕获阶段得到是1,目标阶段是2,冒泡的阶段是
1: 事件捕获 2: 事件目标 3: 事件冒泡
鼠标事件
事件类型
- click:用户单击主鼠标按钮(一般是左键)或者按下在聚焦时按下回车键时触发
- dblclick:用户双击主鼠标按键触发(频率取决于系统配置)mac 系统上触发可能有问题,目前上不知道原因。
- mousedown:用户按下鼠标任意按键时触发 这个很简单,按下就会触发。
- mouseup:用户抬起鼠标任意按键时触发 这个也很简单,抬起就会触发。
- mousemove:鼠标在元素上移动时触发
- mouseover:鼠标进入元素时触发(移动进入就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
- mouseout:鼠标离开元素时触发 (移动离开就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
- mouseenter:鼠标进入元素时触发,该事件不会冒泡(鼠标进入元素时触发(移动进入就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
- mouseleave:鼠标离开元素时触发,该事件不会冒泡(移动离开就会触发,正常不需要点击就能触发,在mac 上的浏览器需要点击才能触发,不知道原因)
var btn = document.querySelector("button")
btn.onclick = function(){
console.log("被点击了!");
}
btn.ondblclick = function(){
console.log("被双击了!");
}
btn.onmousedown = function(){
console.log("按下!");
}
btn.onmouseup = function(){
console.log("抬起");
}
btn.onmousemove = function(){
console.log("移动");
}
btn.onmouseover = function(){
console.log("进入元素:over");
}
btn.onmouseout = function(){
console.log("离开元素:out");
}
btn.onmouseenter = function(){
console.log("进入元素:enter");
}
btn.onmouseleave = function(){
console.log("离开元素:leave");
}
区别:
- over和out,不考虑子元素,从父元素移动到子元素,对于父元素而言,仍然算作离开
- enter和leave,考虑子元素,子元素仍然是父元素的一部分。如果我们平时子元素和当前的元素处理的事件是一致的话,给当前元素注册这个事件就可以了
- mouseenter和mouseleave不会冒泡
stopPropagation 是阻止事件冒泡,阻止的事件就是当前的注册事件,这点很重要。当前元素会触发这个注册事件,不希望他的父元素再触发这个事件了。正常是父元素也会触发这个事件。
div.onmouseover = function(e){
console.log("鼠标进入:over", e)
}
div.onmouseout = function(e){
console.log("鼠标离开:out", e)
}
btn.onmouseover = function(e){
e.stopPropagation();
}
btn.onmouseout = function(e){
e.stopPropagation();
}

- 正常进入div,会触发over,出去会触发out。
- 如果从div进入button,因为上面讲的over 和out 的特点,不考虑子元素,从父元素移动到子元素,对于父元素而言,仍然算作离开,所以会触发一次 out,但是div 还会再触发div 一次 over,这是为什么呢?这是因为进入其实是触发了button 的over事件,因为这个over事件有冒泡,所以会触发div的over事件。这就是根源,怎么解决呢?解决办法就是buton 的onmouseover阻止他冒泡传递给button。同样的原因,button out 的时候也会触发div 的out事件,原因同样是因为冒泡导致的。
- 上面的几行代码就可以真正的做到进入到button就只触发button的方法,进入div只触发div 的相关方法。
对于enter和leave呢?
- 进入div,进入会触发 enter,出去会触发 leave。
- 如果从div进入button,会触发button的enter,但不会触发div 的leave,从button 出去,会触发button的leave,同时也会触发div 的leave。
事件对象
所有的鼠标事件,事件处理程序中的事件对象,都为 MouseEvent
- altKey:触发事件时,是否按下了键盘的alt键
- ctrlKey:触发事件时,是否按下了键盘的ctrl键
- shiftKey:触发事件时,是否按下了键盘的shift键
- button:触发事件时,鼠标按键类型
- 0:左键
- 1:中键
- 2:右键
btn.onmouseup = function(e){
console.log(e.altKey, e.ctrlKey, e.shiftKey);
console.log(e.button);
}
上面的e.altKey e.ctrlKey e.shiftKey 代表事件触发时候用户是否也按着下面这些按键,因此返回值是ture 或者false.
e.button 返回的是数字,0 代表左键,1代表中键,2代表右键。
位置:
- page:pageX、pageY,当前鼠标距离页面的横纵坐标,距离页面,这里包括上面的可滚动区域。
- client: clientX、clientY,鼠标相对于视口的坐标
- offset:offsetX、offsetY,鼠标相对于事件源的内边距的坐标
- screen: screenX、screenY,鼠标相对于屏幕
- x、y,等同于clientX、clientY
- movement:movementX、movementY,只在鼠标移动事件中有效,相对于上一次鼠标位置,偏移的距离
键盘事件
事件类型
- keydown:按下键盘上任意键触发,如果按住不放,会重复触发此事件
- keypress:按下键盘上一个字符键时触发
- keyup:抬起键盘上任意键触发
keydown、keypress 如果阻止了事件默认行为,文本不会显示。
事件对象
KeyboardEvent
键盘事件也有冒泡,因此在控件注册事件的时候尽量给大的组件比方说window,document注册事件。 如果你给一个div注册事件,事件是不会触发的,因为div不能聚焦,除非你给这个div加一个input元素,并且点一下这个input才能触发。
- code:得到按键字符串,适配键盘布局。适配键盘布局代表可以键盘左右区域同样的按键问题。比方说你是按了左边的option还是右边的option的键盘。
- key:得到按键字符串,不适配键盘布局。能得到打印字符。
- keyCode、which:得到键盘编码
<input type="text">
<script>
var inp = document.querySelector("input")
//keycode 得到键盘编码
inp.onkeydown = function(e) {
console.log("键盘按下了", e.code, e.key, e.keyCode)
}
inp.onkeypress = function(e) {
console.log("键盘按下了!key press")
}
inp.onkeyup = function() {
console.log("键盘抬起了!key up")
}
</script>
其他事件
表单事件
- focus:元素聚焦的时候触发(能与用户发生交互的元素,都可以聚焦),该事件不会冒泡。比方说a标签,input。 事件可以通过bubbles,知道该事件是否冒泡。
<div>
<input type="text" class="noinput" value="请输入关键字">
</div>
<script>
var inp = document.querySelector("input")
inp.onfocus = function (e){
console.log("聚焦了",e.bubbles);
}
</script>
- blur:元素失去焦点时触发,该事件不会冒泡。
<style>
.noinput {
color: #ccc;
}
</style>
<div>
<input type="text" class="noinput" value="请输入关键字">
</div>
<script>
var inp = document.querySelector("input")
inp.onfocus = function() {
if (this.value === this.defaultValue && this.className === "noinput") {
this.value = "";
this.className = "";
}
}
inp.onblur = function() {
if (!this.value) {
this.value = this.defaultValue;
this.className = "noinput";
}
}
</script>
- submit:提交表单事件,仅在form元素有效。
<form action="https://baidu.com">
<p><input type="text" placeholder="请输入账号"></p>
<button>按钮一</button> <button>按钮二</button>
</form>
<script>
var form = document.forms[0];
var inp = document.querySelector("input");
form.onsubmit = function (e){
console.log(e.bubbles);
if(!inp.value.trim()){
e.preventDefault();
console.log("请输入账号");
}
}
</script>
- change:文本改变事件
- input: 文本改变事件,即时触发
其他事件
window全局对象
- load、DOMContentLoaded、readystatechange
window的load:页面中所有资源全部加载完毕的事件
图片的load:图片资源加载完毕的事件
浏览器渲染页面的过程:
- 得到页面源代码
- 创建document节点
- 从上到下,将元素依次添加到dom树中,每添加一个元素,进行预渲染
- 按照结构,依次渲染子节点
document的DOMContentLoaded: dom树构建完成后发生
readystate(准备的状态): loading(正在加载)、interactive(可交互)、complete(完成)
interactive:触发DOMContentLoaded事件
complete:触发window的load事件
渐变结束的事件:
js代码应该尽量写到页面底部
- css应该写到页面顶部:避免出现闪烁(如果放到页面底部,会导致元素先没有样式,使用丑陋的默认样式,然后当读到css文件后,重新改变样式)
- JS应该写到页面底部:避免阻塞后续的渲染,也避免运行JS时,得不到页面中的元素。
一道简单的面试题
//loading
console.log(document.readyState);
//interactive
document.addEventListener("DOMContentLoaded", function () {
console.log(document.readyState);
})
//complete
window.onload = function(){
console.log(document.readyState);
}
如何正确获取img的尺寸?
document.addEventListener("DOMContentLoaded", function () {
var img = document.querySelector("img");
getImgSize(img, function (size) {
console.log(size);
});
})
function getImgSize(img, callback) {
if (img.width === 0 && img.height === 0) {
img.onload = function () {
callback({
width: img.width,
height: img.height
});
}
}
else {
callback({
width: img.width,
height: img.height
});
}
}
- unload、beforeunload
beforeunload: window的事件,关闭窗口时运行,可以阻止关闭窗口 unload:window的事件,关闭窗口时运行
- scroll 窗口发生滚动时运行的事件 通过scrollTop和scrollLeft,可以获取和设置滚动距离。 这个事件不仅可以给window设置,也可以给元素设置值,类似于UIScrollview的offset. 其中scrollLeft相当于offset.x,scrollTop相当于offset.y.
<p>
dolor.此处1000个单词
</p>
<div>
<button>回到顶部</button>
<button>到底部</button>
</div>
<script>
window.onscroll = function(){
console.log(document.documentElement.scrollTop + document.body.scrollTop);
}
var p = document.querySelector("p");
p.onscroll = function() {
console.log(this.scrollTop, this.scrollLeft);
}
document.getElementsByTagName("button")[0].onclick = function() {
p.scrollTop = 0;
p.scrollLeft = 0;
}
document.getElementsByTagName("button")[1].onclick = function() {
p.scrollTop = p.scrollHeight;
p.scrollLeft = p.scrollWidth;
}
</script>
注意的是给window注册onscroll滚动事件,要用document.documentElement 和document.body相加去获取,因为其中一个可能是0. 上面的代码后面是给他设置值。
- resize 窗口尺寸发生改变时运行的事件,监听的是视口尺寸,注意这个事件只能给window注册,别的元素注册没有用。
window.onresize = function () {
}
window尺寸相关的参数具体指代
默认情况width是内容宽度。如果设置了boder-box 是加上border padding 后的宽度。
注意:offsetWidth 和clientWidth差别就差在滚动条上。
- contextmenu 右键菜单事件,可以给元素注册事件,也可以给window注册事件。
- paste 粘贴事件
- copy 复制事件
- cut 剪切事件
元素位置
-
offsetParent 获取某个元素第一个定位的祖先元素,如果没有,则得到body body的offsetParent为null
-
offsetLeft、offsetTop 相对于该元素的offsetParent的坐标 如果offsetParent是body,则将其当作是整个网页
-
getBoundingClientRect方法 该方法得到一个对象,该对象记录了该元素相对于视口的距离
一个吸附效果的代码实现
var div = document.querySelector(".container");
var scrollTop = 0;
window.onscroll = function() {
var rect = div.getBoundingClientRect();
if (rect.top < 0) {
div.style.position = "fixed";
div.style.top = 0;
div.style.left = rect.left + "px";
div.style.width = rect.width + "px";
div.style.height = rect.height + "px";
div.style.boxSizing = "border-box";
//记录滚动条的位置
scrollTop = window.scrollY + rect.top;
}
if (window.scrollY < scrollTop) {
div.setAttribute("style", "");
}
}
div.setAttribute("style", "") 相当于清空div的内部样式。
其实实现悬浮效果的核心逻辑是监听window的滚动,rect.top 表示div距离视口顶部的距离,rect.top < 0表示上边缘滚动到了视口的上面去了,这是就可以设置让他固定了,此时还有一个问题就是要记录一下,这个其实就是当前div上边缘的距离整个内容的位置。其实就是鼠标事件里面的pageY。这里可以通过window.scrollY 减去上边缘滚超的rect.top来计算。
事件模拟
可以用代码模拟用户的各种操作,点击按钮,提交表单,鼠标操作等事件。
- click
- sumbit
- dispatchEvent
其他补充
- window.scrollX、window.pageXOffset、window.scrollY、window.pageYOffset
window.scrollX、window.pageXOffset: 相当于根元素的scrollLeft window.scrollY、window.pageYOffset: 相当于根元素的scrollTop
-
scrollTo、scrollBy scrollTo: 手动设置滚动条位置,代表是整体值。 scrollBy(x,y)是代表原来的基础上增加了多少,代表的是偏移量。
-
resizeTo、resizeBy 差别上面的大小类似,一个是整体值,一个是偏移量,只有通过代码打开的窗口才能控制。
实践中一个注意点
上面的imgNumber无法用上面的写法,为什么呢?
赋值语句尚未完成,所以下面的写法也不行。因为运行右边语句的时候config还是undefined。
也不能使用this,因为this使用在函数中,这里不是函数,不能使用。这里如果用了是指向全局对象window。 this可以在两种情况下使用: 1.全局环境 2.函数里面
正确的做法是配置完对象之后再设置。
其实这就是在对象初始化的时候,因为一个属性要使用另外一个属性的问题,解决办法就是初始化完成后再去设置另外一个属性。
而下面这种写法是可以的。