JavaScript DOM 事件初探

原文链接

事件,就是文档或浏览器窗口中发生的一些特定的交互瞬间,比如单击、双击、鼠标悬浮等。

事件流

  • 事件流描述的是从页面中接收事件的顺序,或者说是事件在页面中传播的顺序。

    • IE的事件流叫做事件冒泡(event bubbling):由最具体的元素开始执行事件,然后逐级向上传播到 window 对象。

    • 网景团队提出的事件流叫做事件捕获(event capturing):由最外层的 window 对象开始执行事件,然后逐渐向下传播到最具体的元素。

下面我们做个小实验来说明这两种事件流的不同之处:

// html 文档
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>事件流 实验</title>
</head>
<body>
  <div id="test" style="font-size:3em;">点我点我,我是div</div>
</body>
</html>
// 事件冒泡
var div = document.getElementById("test");

div.addEventListener("click",function(){
  console.log("i am div");
}, false);
document.body.addEventListener("click",function(){
  console.log("i am body");
}, false);
document.documentElement.addEventListener("click",function(){
  console.log("i am html");
}, false);
document.addEventListener("click",function(){
  console.log("i am document");
}, false);
window.addEventListener("click",function(){
  console.log("i am window");
}, false);

clipboard.png

<pre>
点击div,控制端会打印如下:
i am div
i am body
i am html
i am document
i am window
</pre>

// 事件捕获
var div = document.getElementById("test");

div.addEventListener("click",function(){
  console.log("i am div");
}, true);
document.body.addEventListener("click",function(){
  console.log("i am body");
}, true);
document.documentElement.addEventListener("click",function(){
  console.log("i am html");
}, true);
document.addEventListener("click",function(){
  console.log("i am document");
}, true);
window.addEventListener("click",function(){
  console.log("i am window");
}, true);

clipboard.png

图片经过二次处理,略丑,请见谅!

<pre>
点击div,控制端会打印如下:
i am window
i am document
i am html
i am body
i am div
</pre>

可以看出,事件冒泡和事件捕获是两种完全相反的事件流

平时,一般都是在使用事件冒泡,只在特殊情况下使用事件捕获。

  • DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。

// 事件冒泡和事件捕获混合一下
var div = document.getElementById("test");

div.addEventListener("click",function(){
  console.log("i am div");
}, true);
document.body.addEventListener("click",function(){
  console.log("i am body");
}, false);    // 改为在冒泡阶段调用事件处理程序
document.documentElement.addEventListener("click",function(){
  console.log("i am html");
}, true);
document.addEventListener("click",function(){
  console.log("i am document");
}, true);
window.addEventListener("click",function(){
  console.log("i am window");
}, false);    // 改为在冒泡阶段调用事件处理程序

这个图片是直接从书上摘得,自己脑补一下,把 window 对象自行添上去吧!

clipboard.png

<pre>
点击div,控制端会打印如下:
i am document
i am html
i am div
i am body
i am window
</pre>

事件处理程序

事件处理程序:指的是响应处理某个事件的函数

  • HTML 事件处理程序

某个元素支持的每种事件,都可以使用一个与相应事件处理程序同名的 HTML 特性来指定。

// 情况一
<button onclick="alert('I am button!');">按钮</button>

// 情况二
<button onclick="show();">按钮</button>
<script>
function show(){
  alert("I am button!");
}
</script>

建议永远不要使用这种方式为元素添加事件,因为这种方法有一个巨大的缺点,就是使得 HTML 与 JavaScript 的代码紧密耦合,不符合网页设计的行为与结构分离。

  • DOM0 级事件处理程序

每个元素(包括 window 和 document)都有自己的事件处理程序属性,比如 onclick、onmouseup 等。

把上面的代码改写如下:

<button id="test">按钮</button>

var button = document.getElementById("test");
button.onclick = function(){
  // this 对象指向 button 元素
  console.log(this);
  alert("I am button!");
};

/*  删除指定的事件处理程序  */
button.onclick = null;
  • DOM2 级事件处理程序

DOM2 级事件 定义了两个方法,用来处理指定和删除事件处理程序的操作:addEventListener()removeEventListener()

这两个方法都有三个参数,第一个参数是事件名,第二个参数是作为事件处理程序的函数,第三个参数是一个布尔值,默认值是 false,表示在冒泡阶段调用事件处理程序,如果这个值是 true,表示在捕获阶段调用事件处理程序。

继续改写上面的代码:

var button = document.getElementById("test");

function show(){
  // this 对象指向 button 元素
  console.log(this);
  alert("I am button!");
}
// 添加事件处理程序
button.addEventListener("click",show, false);
button.addEventListener("click",function(){
  alert("I am second alert!");
},false);
// 删除事件处理程序
button.removeEventListener("click",show, false); // 删除成功
button.removeEventListener("click",function(){   // 删除失败
  alert("I am second alert!");
},false); 

使用 DOM2 级方法添加事件处理程序的主要好处是可以为一个元素添加多个事件处理程序。

如果 addEventListener() 添加的事件处理函数是匿名函数,则无法通过 removeEventListener() 删除这个事件处理程序。

  • IE 事件处理程序

IE8 以及更早的 IE 版本,只支持事件冒泡,不支持 addEventListener() 和 removeEventListener(),但是它实现了与这两个方法类似的两个方法:attachEvent()detachEvent()。这两个方法只有两个参数,一个事件名称(是 onclick,不是 click),一个事件处理函数。

与 addEventListener() 不同,使用attachEvent() 方法的情况下,事件处理函数会在全局作用域下运行,所以 this 等于 window,这一点需要特别注意。

var button = document.getElementById("test");

button.attachEvent("onclick", function(){
  // this 对象指向了全局作用域,即 window
  console.log(this);
  alert("I am button!");
});
button.attachEvent("onclick",function(){
  alert("I am second alert!");
}); 
// 上面也是为同一个按钮绑定了两个事件处理程序

button.detachEvent("onclick",function(){ // 删除失败
  alert("I am second alert!");
}); 

除非你想使你的程序兼容至 IE8- 浏览器,否则不要使用这两个函数。

浏览器默认行为

对于一些特定的事件,浏览器有它默认的行为。比如:点击链接会进行跳转到指定的页面、点击鼠标右键会呼出浏览器右键菜单、填写表单时按回车会自动提交到服务器等。

默认的浏览器行为和冒泡行为是独立的,取消事件默认行为是不会取消事件冒泡的,反之亦然。同一个元素的多个事件处理函数也是相互独立的。

<div>
  <a id="test" href="http://baidu.com">点我点我,我是链接</a>
</div>

var link = document.getElementById("test");

// 方法一   event.preventDefault()
link.onclick = function(event){
  console.log("You click!");
  event.preventDefault();
};

// 方法二   return false
link.onclick = function(event){
  console.log("You click!");
  return false;
};

参考:事件-4. 浏览器默认行为-宇卿

事件对象 event

在触发 DOM 上的某个事件时,会产生一个事件对象 event,这个对象中包含着所有与该事件有关的信息。

event 对象只存在于事件处理程序执行期间,一旦执行完毕,立即被销毁。

<div>
  <a id="test" href="http://baidu.com">点我点我,我是链接</a>
</div>


/* 下列 event 对象的属性和方法都是只读的  */

var link = document.getElementById("test");
link.onclick = function(event){
  // 判断当前事件是否会向 DOM 树上层元素冒泡
  console.log(event.bubbles);
  
  // 判断是否可以取消事件的默认行为
  console.log(event.cancelable);
  // 使用该方法可以取消事件的默认行为(使用前提是 cancelable 属性的值为 true)
  event.preventDefault();
  // 判断是否已经调用了 preventDefault() 方法(DOM3级事件新增)
  console.log(event.defaultPrevented);
  
  // 指向事件遍历 DOM 时,识别事件的当前目标对象
  console.log(event.currentTarget);
  // 指向触发事件的对象
  console.log(event.target);
  
  // 表示事件流当前处于哪一个阶段
  // 值为 1 表示在捕获阶段,值为 2 表示处于目标阶段,值为 3 表示在冒泡阶段
  console.log(event.eventPhase);
  
  // 返回一个字符串, 表示该事件对象的事件类型
  console.log(event.type);
  
  // 立即停止当前事件在 DOM 层次中的传播,即取消进一步的事件捕获或冒泡
  event.stopPropagation();
};

为了进一步说明 event.stopPropagation() 的运行效果,借用前面的代码,更改如下:

// 事件冒泡和事件捕获混合一下
var div = document.getElementById("test");

div.addEventListener("click",function(){
  console.log("i am div");
}, true);
document.body.addEventListener("click",function(){
  console.log("i am body");
}, false);    // 改为在冒泡阶段调用事件处理程序
document.documentElement.addEventListener("click",function(){
  console.log("i am html");
  event.stopPropagation();    // 立即停止事件在 DOM 中的传播
}, true);
document.addEventListener("click",function(){
  console.log("i am document");
}, true);
window.addEventListener("click",function(){
  console.log("i am window");
}, false);    // 改为在冒泡阶段调用事件处理程序

<pre>
点击div,控制端会打印如下:
i am document
i am html
</pre>

看到了吧,后面元素的事件就不会被激发了。

事件类型

UI 事件

UI 事件指的是简单的用户界面事件

  • load 事件:当页面所有资源(比如图像、css文件、js文件等资源)完全加载完毕后,就会触发 window 上面的 load 事件。

当然,我们也可以单独为某个元素设置 load 事件,比如为一个图片绑定这个事件,就可以检测这个图片是否加载完毕了。

<img id="myImg" src="1.jpg" />

var img = document.getElementById("myImg");
img.onload = function(){
  alert("Image loaded!");
};
  • scroll 事件:在文档被滚动期间,重复触发该事件。

window.onscroll = function(){
  console.log("Scroll!");
};
焦点事件

焦点事件会在页面元素获得或失去焦点时触发。

注意:默认情况下,只有部分 html 元素能获得鼠标焦点(如 input,a),很大一部分 html 元素是不能获得鼠标焦点的(如 div,img),这些能够获得鼠标焦点的元素就是 focusable 元素。

不过,可以通过为这些默认没有焦点事件的元素添加一个 tabindex 属性,从而使它可以支持焦点事件。

  • document.activeElement:返回当前页面中获得焦点的元素,如果没有某个元素获得焦点,则该属性的值为当前页面中的<body>元素

  • document.hasFocus():判断当前文档或者当前文档的子节点是否获得了焦点

  • HTMLElement.focus():使得指定的元素获得焦点(前提是这个元素是一个可以获得焦点的元素)

  • HTMLElement.blur():移除当前元素获得的焦点

上面是一些 HTML5 中添加的焦点管理 API。下面说焦点事件:

  • focus 事件:在元素获得焦点时触发,这个事件不会冒泡

  • blur 事件:在元素失去焦点时触发,这个事件不会冒泡

  • focusin 事件:在元素获得焦点时触发,与 focus 事件等价,但会冒泡

  • focusout 事件:在元素失去焦点时触发,与 blur 事件等价,但会冒泡

<img id="img" src="1.jpg" tabindex="1" />

var img = document.getElementById("img");
document.body.onscroll = function() {
    img.blur();
};
img.onfocus = function() {
    console.log("Img focused!");
};
img.onblur = function() {
    console.log("Img lose focus!");
};
img.onclick = function() {
    console.log(document.activeElement);
    console.log(document.hasFocus());
};

注意: focusin 和 focusout 事件,所有的 Firefox 版本都不支持,看这里。chrome 和 safari 中只有通过 addEventListener 方式绑定这两个事件才能正常使用,其他方式绑定都不行。

说说focus /focusin /focusout /blur 事件(推荐)
(WHATWG)Focus management APIs

鼠标与滚轮事件

DOM3 级事件中定义了9个鼠标事件。

<body>
 <div id="test">
   <button>按钮</button>
 </div>
</body>

// css
div {
    width: 300px;
    height: 300px;
    margin: 50px auto;
    border: 1px solid #aaa;
  }
  • mouseover 事件:鼠标指针首次进入一个元素边界之内时触发,会冒泡

  • mouseout 事件:鼠标指针移出这个元素边界时触发,会冒泡

var test = document.getElementById("test");

test.onmouseover = function(event){
  event.target.style.border = "3px solid #666";
};
test.onmouseout = function(event){
  event.target.style.border = "none";
};
  • mouseenter 事件:鼠标指针首次移动到元素范围之内时触发,不冒泡

  • mouseleave 事件:鼠标指针移动到元素范围之外时触发,不冒泡

var test = document.getElementById("test");

test.onmouseenter = function(event){
  event.target.style.border = "10px solid #666";
};
test.onmouseleave = function(event){
  event.target.style.border = "none";
};
  • mousemove 事件:当鼠标在元素内部移动时重复触发,会冒泡

// 这里写一个鼠标在 body 内移动时,背景颜色随机变化的脚本

document.body.onmousemove = function(){
  var r = Math.floor(Math.random() * 256)
      g = Math.floor(Math.random() * 256)
      b = Math.floor(Math.random() * 256);
  document.body.style.background = "rgb("+r+","+g+","+b+")";
};
  • mousedown 事件:用户通过按下任意鼠标按钮时触发

  • mouseup 事件:用户释放鼠标按钮时触发

  • click 事件:用户单击鼠标左键时或按下回车键时触发

  • dblclick 事件:用户双击鼠标左键时触发

var body = document.body;
body.onmousedown = function(){
  console.log("Mouse down!");
};
body.onmouseup = function(){
  console.log("Mouse up!");
};
body.onclick = function(){
  console.log("One click!");
};
body.ondblclick = function(){
  console.log("Double click!");
};
// 双击 body,控制端打印:
Mouse down!
Mouse up!
One click!
Mouse down!
Mouse up!
One click!
Double click!

只有在同一个元素上相继触发 mousedown 和 mouseup 事件,才会触发 click 事件,缺一不可。类似地,只有连续触发两次 click 事件,才会触发 dblclick 事件。

  • 获取点击坐标

    • clientX 和 clientY:鼠标指针在视口中的坐标

    • pageX 和 pageY:鼠标指针在页面中的坐标

    • screenX 和 screenY:鼠标指针在屏幕中的坐标

在页面没有滚动的情况下,pageX 和 pageY 的值与 clientX 和 clientY 的值相等。

document.body.onclick = function() {
  console.log("Client: " + "(" + event.clientX + "," + event.clientY + ")");
  console.log("Page: " + "(" + event.pageX + "," + event.pageY + ")");
  console.log("Screen: " + "(" + event.screenX + "," + event.screenY + ")");
};
键盘与文本事件
  • keydown 事件:当用户按下任意键时触发,而且如果按住不放的话,会重复触发此事件

  • keypress 事件:当用户按下任意字符键时触发,而且如果按住不放的话,会重复触发此事件

  • keyup 事件:当用户释放键盘上的按键时触发

在用户按下一个字符键并且立马释放这个按键的过程中,先触发 keydown 事件,再触发 keypress 事件,最后触发 keyup 事件。

window.onkeydown = function() {
  console.log("on key down");
};
window.onkeyup = function() {
  if (event.keyCode === 65) {
    console.log(event.key);
  }
  console.log("one key up");
};
window.onkeypress = function() {
  console.log("one key press");
};

为了知道自己按下的是哪个按键,可以使用 event.which 来获得你按下按键的键码(比如字母 A 的键码为 65)。也可以通过键码属性(event.keyCode)来对特定的按键来进行响应。键码参考表

发生 keypress 事件时,会存在一个 charCode 属性,返回这个按键代表字符的 ASCII 编码。ASCII table

event.which 返回一个 keyCode 或 charCode 值,详情看下面代码

String.fromCharCode() 静态方法可以将 event.which 转化为相应的字符。

// 请自行粘贴复制于浏览器控制端测试
// 分别输入 a 和 A
// A和a 的 ASCII码分别为65,97

window.onkeydown = function(){
  if(event.keyCode === 65){
    console.log("(keydown keyCode)You press 'a' or 'A'!");
  }
  if(event.charCode === 65){
    console.log("(keydown charCode)You press 'A'!");
  }
  console.log("keydown event.which = " + event.which);
  console.log("(keydown:)" + String.fromCharCode(event.which));
};
window.onkeypress = function(){
  if(event.keyCode === 65){
    console.log("(keypress keyCode)You press 'A'!");
  }
  if(event.charCode === 65){
    console.log("(keypress charCode)You press 'A',not 'a'!");
  }
  console.log("keypress event.which = " + event.which);
  console.log("(keypress:)" + String.fromCharCode(event.which));
};

在 DOM3 级事件中,做了一些变化,不再推荐使用 keyCode、charCode、which 属性,而是出现了两个新属性:keychar,用来替代 keyCodecharCode 属性。

但是不建议使用 key、char 属性,因为有一部分浏览器对他支持的不是很好。点这里

参考:JavaScript 事件——“事件类型”中“键盘与文本事件”的注意要点

变动事件

变动事件是在 DOM 结构发生变化时触发

在 DOM2 中定义了多个变动事件,但是在 DOM3 中又废除了一些,在这里先简单提一下有这个东西,以后用到再作补充。

HTML5 事件
  • contextmenu 事件:单击鼠标右键,会触发这个事件并调出页面的上下文菜单。这个事件是鼠标事件的一种,并且支持冒泡。

<p data-height="265" data-theme-id="0" data-slug-hash="jryBBG" data-default-tab="js,result" data-user="percy507" data-embed-version="2" class="codepen">See the Pen context menu test by percy (@percy507) on CodePen.</p>
<script async src="//assets.codepen.io/assets/embed/ei.js"></script>

在上面的例子中,我们演示了利用 contextmenu 事件来设置自定义右键菜单,以及屏蔽右键菜单。

然而,屏蔽右键菜单却难不倒高手,Greasy Fork 上面有一个脚本(网页限制解除),专门来打破对右键菜单有限制的页面。

  • beforeunload 事件:当浏览器窗口,文档或其资源将要卸载时或者刷新页面时,会触发这个事件【MDN】

下面有几点需要注意一下:

1. 如果处理函数为 Event 对象的 returnValue 属性赋值非空字符串,浏览器会弹出一个对话框,来询问用户是否确定要离开当前页面。没有赋值时,该事件不做任何响应。

2. 从2011年5月25号开始,HTML5规范指出在此事件处理函数中,对于window.alert(), window.confirm(), 和 window.prompt() 的调用会被忽略。

3. 旧版本的浏览器可能会在提示框里显示返回的信息,但是新版本浏览器都默认采用浏览器内设的提示信息。

// 复制粘贴于浏览器控制端,然后关闭页面,会出现提示框
window.addEventListener("beforeunload", function() {
  var message = "你真的要离开吗?";
  event.returnValue = message;
  return message;
}, false);
  • DOMContentLoaded 事件:在页面文档形成完整的 DOM 树时触发,不理会图像、js文件、css文件等资源是否加载完毕

这个事件可以为 document 或 window 绑定,但实际目标是 document。这个事件会冒泡。

注意:这个事件与 load 事件是不一样的,正常情况下,会早于 load 事件被触发。

<p data-height="265" data-theme-id="0" data-slug-hash="PGWXKz" data-default-tab="js,result" data-user="percy507" data-embed-version="2" class="codepen">See the Pen DOMContentLoaded & load by percy (@percy507) on CodePen.</p>
<script async src="//assets.codepen.io/assets/embed/ei.js"></script>

感觉篇幅太长了,事件委托移动设备中的事件事件对内存性能的影响等内容将在以后逐渐写出。

参考资料

  • 【书】《JavaScript 高级程序设计(第三版)》


hjxja890980
962 声望54 粉丝

nothing.