【笔记】JavaScript事件处理机制,元素大小判断与H5的媒体标签

前一阵舍友去面试,被问到JavaScript中的事件处理机制。暗自思忖,发现自己也没有深入的了解过。顺带连同常用的HTML元素大小和实际中用到的HTML5中的媒体元素简单整理在下面,方便之后回顾。

事件

JavaScript和HTML的交互是通过事件实现的。可以通过监听器订阅文档或窗口中的事件,在事件发生时执行特定的代码。这种属于设计模式中的观察者模式。

事件相关的API最早出现在IE4和NetScape Nivagator4(后面简称为网景)中。两种浏览器提供了相似却不同的API。在之后的DOM2级标准中对DOM事件进行了标准化。

事件流

事件流描述的是页面中接受时间的顺序。在这点上IE和网景采用了完全相反的两种处理思路。IE采用的是事件冒泡流,网景采用的是事件捕获流

事件冒泡(event bubbling)指从事件开始的最具体的元素接收,再逐步向上传递到最外层的节点,直到document。如下图(来自红宝书)展示的过程,在下面的文档中:

1
2
3
4
5
6
7
8
9
<!DOCTYPE html>
<html>
<head>
<title>Event bubbling</title>
</head>
<body>
<div>Click me</div>
</body>
</html>

如果div标签被点击,click事件会这样依次传递:<div> -> <body> -> <html> -> document。(不同浏览器实现细节上会有不同)

事件捕获(event capturing)则认为应该从父节点开始捕获事件直到事件目标。因此,同样的上面的例子,顺序将是:document -> <html> -> <body> -> <div>

目前很少有人使用事件捕获这种方式作为事件流。

DOM 事件流

“DOM2级标准”中规定事件流包括三个阶段,事件捕获处于目标时间冒泡。在实际的DOM事件流中,实际目标不会接受到事件。因此如下图展示的那样,捕获阶段停止在父目标<body>上,之后事件发生在目标上,并作为事件冒泡的一部分。然后,冒泡阶段发生,事件传回到文档。

事件处理程序

事件处理程序指用户指定响应事件的某种动作。它们都以’on’开头。HTML元素本身都可以使用与之同名的HTML特性。

DOM0级事件处理程序

DOM0级事件处理程序就是将一个函数直接赋值给一个事件处理程序属性。使用这种方法指定的事件处理程序被认为是元素的一种方法,从而其作用域为元素本身,即this指向引用元素。可以通过直接为事件处理程序属性赋值为null删除。

1
2
3
4
5
6
var btn = document.getElementById('button');
btn.onclick = function () {
alert(this.id); // "button"
}
//删除
btn.onclick = null;

所有浏览器都支持DOM0级事件处理程序。这么做的好处是可以保证浏览器兼容性,缺点是使得HTML和JavaScript紧密耦合,不利用后期维护。

DOM2级事件处理程序

伴随DOM2级标准提出,“DOM2级事件”提出了两种方法,用于绑定和解除事件处理程序:addEventListener()removeEventListener()。它接受3个参数:事件名事件处理程序对应的函数表示捕获阶段的布尔值

1
2
3
4
5
6
7
var btn = document.getElementById('button');
btn.addEventListener("click", function () {
alert(this.id);
}, false);
btn.addEventListener("click", function () {
alert(this.id + " again.");
}, false);

使用DOM2级方法绑定事件处理程序的一个优点是,可以添加多个程序到同一个标签上。使用DOM0级方法时则会覆盖上一次的事件处理程序。IE9及以上版本都支持DOM2级事件处理程序。

由于IE事件处理程序在IE8之前,是通过类似的attachEvent()detachEvent()方法。它的第一个参数是事件名(需要带上on),第二个参数是事件处理程序。通过这种方法绑定的处理程序都添加在冒泡阶段,且需要注意的是其中的this等于window对象。支持这种方式有IE和Opera。

因此,一个跨浏览器兼容的事件绑定和解绑应该是下面这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
var EventUtil = {
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;
}
},
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;
}
},
}

===

元素大小与位置

这些属性方法并不属于“DOM2级样式”,但是却经常得到使用。目前所有主流浏览器都支持这些属性。它们大多都是只读的。

偏移量

偏移量描述元素在屏幕中占用的可用空间,由其宽高决定,包括内边距、滚动条和边框(不包括外边距)。有下面4个属性:

  • offsetHeight 元素垂直方向上的占用空间
  • offsetWidth 元素水平方向上的占用空间
  • offsetLeft 元素左边框距offsetParent元素左内边框的像素距离
  • offsetTop 元素上边框距offsetParent元素上内边框的像素距离

可以利用元素的offsetLeftoffsetLeft与其offsetParent对应属性相加直到根元素,获取到元素相对于页面的左偏移值或上偏移值。

客户区大小

客户区大小指元素内容和内边距占据的空间大小,不包括滚动条。clientWidth是元素内容宽度加左右内边距的宽度,clientHeight是元素内容高度加上下内边距的高度。

可以通过对body元素取值来获取当前浏览器视口的大小。

滚动大小

滚动大小包含滚动内容的元素大小。它有下面4个相关属性:

  • scrollHeight 没有滚动条时,元素内容的高度
  • scrollWidth 没有滚动条时,元素内容的宽度
  • scrollLeft 被隐藏在内容区域左侧的像素数,可以设置从而改变元素滚动位置
  • scrollTop 被隐藏在内容区域上侧的像素数,可以设置从而改变元素滚动位置

scrollHeight/scrollWidthclientHeight/clientWidth在不同浏览器下的表现行为并不相同,有的表示视口大小,有的表示元素内容区域大小。使用时可以取较大值。而另外两个属性scrollLeftscrollTop则通常用在document中,获取和滚动相关的属性。

确定元素大小

大多数主流浏览器为元素提供了getBoundingClientRect()方法,返回一个对象,包含leftrighttopbottom四个属性。给出了元素相对于视口的位置。

对不支持这个方法的浏览器,可以通过偏移量的相关属性获取。

===

媒体元素

HTML5出现前,提供富媒体内容的网站多采用Flash的方式保证浏览器兼容性。HTML5新增了两个标签<audio><video>。用于方便地嵌入音频和视频内容。同时,这两个标签也提供了实现常用功能的JavaScript API。允许为媒体创建自定义控件。

1
2
<video src="demo.mpg" id="foo">Video player is not available.</video>
<audio src="song.mp3" id="bar">Audio player is not available.</audio>

其中元素的src属性指定了加载的媒体文件,还可以通过widthheight属性指定播放器大小。controls属性意味浏览器应该显示UI控件用于操作媒体。标签中的内容用于在不支持时显示后备内容。

因为不同浏览器支持的媒体格式集并不完全相同,可以在标签下指定一或多个<source>元素,通过srctype属性指定来源和格式,视频标签下<source>type中甚至可以指定codecs表示解码器。目前现代浏览器(IE9+,对IE说的就是你)都支持这两个标签。

1
2
3
4
5
6
7
8
<video id="myVideo">
<source src="foo.mpg">
<source src="foo.webm" type="video/webm; codecs=vp8, vorbis">
</video>
<audio>
<source src="song.ogg" type="audio/ogg">
<source src="song.mp3" type="audio/mpeg">
</audio>

属性

<video><audio>提供了完善的JavaScript接口,下面是一些可能会用到的它们的属性。其中很多可以直接在标签元素上设置。

  • autoplay 取消或设置当前autoplay标识
  • controls 取消或设置当前controls标识,用于显示和隐藏浏览器内置控件
  • currentTime 获取已经播放的秒数
  • duration 获取媒体的总长度(秒数)
  • ended 获取媒体是否播放完成
  • loop 取消或设置媒体文件是否循环播放
  • muted 取消或设置媒体文件是否静音
  • paused 标识播放器是否暂停
  • playbackRate 取消或设置当前播放速度
  • readyState 标识媒体是否就绪,有0,1,2,3四种情况,表示不可用、可以播放当前帧、可以播放、加载完毕
  • src 媒体文件来源,可重写
  • volume 取消或设置当前音量,值为0.0到1.0

事件

这两个媒体元素还有许多事件,有的是媒体播放的结果,有的是用户操作的结果。

  • abort 下载中断
  • canplay 对应着readyState为2
  • canplaythrough 对应着readyState为3
  • ended 媒体播放完毕
  • error 下载过程网络错误
  • pause 播放暂停
  • play 媒体收到播放指令
  • playing 媒体开始播放
  • ratechange 播放速度改变
  • seeked 移动到新位置
  • seeking 正在移动进度条
  • volumnchange volumnmuted属性值改变
  • waiting 播放因下载未完成暂停

在如此丰富的属性和事件的帮助下,结合play()pause()方法,我们可以很容易构建一个自定义的媒体播放器。

1
2
3
4
5
6
7
8
9
10
11
<div class="player">
<div class="player__content">
<video id="video" src="movie.mov" poster="movie.jpg" width="400" height="200">
Video is not supported.
</video>
</div>
<div class="player__control">
<input type="button" value="Play!" id="video-play">
<span id="curtime">0</span>/<span id="duration">0</span>
</div>
</div>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
var player = document.getElementById("video"),
btn = document.getElementById("video-play"),
curtime = document.getElementById("curtime"),
duration = document.getElementById("duration");
//更新播放时间
duration.innerHTML = player.duration;
//为按钮添加处理事件
EventUtil.addHandler(btn, "click", function (e) {
if (player.paused) {
player.play();
btn.value = "Pause!";
} else {
player.pause();
btn.value = "Play!";
}
});
//定时更新时间
setInterval(function () {
curtime.innerHTML = player.currentTime;
}, 250);

最后,不是所有浏览器都支持这两个标签的所有解码器,因此有一个API来检测浏览器是否支持某种解码器。通过canPlayType()方法,该方法接收格式/编解码器(如”audio/wav“)字符串,返回”probably”, “maybe”或是空字符串””。像下面这样:

1
2
3
if (audio.canPlayType("audio/mpeg")){
//进一步处理
}