事件发生时,你在想什么?

902 阅读50分钟

本章内容:

  • 理解事件流
  • 使用事件处理程序
  • 不同的事件类型

事件流

事件流描述的是从页面中接受事件的顺序。但有意思的是,IE和Netscape开发团队居然提出了差不多完全相反的事件流的概念。IE的事件流是事件冒泡流,而Netscape Communicator的事件流是事件捕获流。

事件冒泡(event bubbling)

即事件开始时由最具体的元素接收,然后逐级向上传播到较为不具体的节点。

所有现代浏览器都支持事件冒泡,但在具体实现上还是有一些差别。IE5.5及更早版本中的事件冒泡会跳过<html>元素(从<body>直接跳到document)。IE9、Firefox、Chrome和Safari则将事件一致冒泡到window对象。

事件捕获(event capturing)

事件捕获的思想是不太具体的节点应该更早接收到事件,而最具体的节点应该最后接收到事件。

DOM事件流

DOM2级事件规定的事件流包括三个阶段:事件捕获阶段、处于目标阶段和事件冒泡阶段。首先发生的是事件捕获,为截获事件提供了机会。然后是实际的目标接收到事件。最后一个阶段是冒泡阶段,可以在这个阶段对事件做出响应。
多数支持DOM事件流的浏览器都实现了一种特定的行为;即使“DOM2级事件”规范明确要求捕获阶段不会涉及事件目标,但IE9、Safari、Firefox和Opera9.5及更高版本都会在捕获阶段触发事件对象上的事件。结果,就是有两个机会在目标对象上面操作事件。

事件处理程序

事件就是用户或浏览器自身执行的某种动作。诸如click、load和mouseover,都是事件的名字。而响应某个事件的函数就是事件处理程序。事件处理程序的名字以“on”开头,因此click事件的事件处理程序就是onclick,load事件的事件处理程序就是onload。为事件指定处理程序的方式有好几种。

HTML事件处理程序

<script type='text/javascript'>
    function showMessage() {
        alert('hello world');
    }
</script>
<input type='button' value='Click Me' onclick='showMessage()' />

在这个例子中,单击按钮就会调用showMessage()函数。这个函数是在一个独立的<script>元素中定义的,当然也可以被包含在一个外部文件中。事件处理程序中的代码在执行时,有权访问全局作用域中的任何代码。

这样指定事件处理程序具有一些独到之处。首先,这样会创建一个封装着元素属性值的函数。这个函数中有一个局部变量event,也就是事件对象:

<input type='button' value="Click Me" onClick='alert(event.type)'>

通过event变量,可以直接访问事件对象,你不用自己定义它,也不用从函数的参数列表中读取。在这个函数内部,this值等于事件的目标元素。

<input type='button' value='Click Me' onclick='alert(this.value)'>

关于这个动态创建的函数,另一个有意思的地方是它扩展作用域的方式。在这个函数内部,可以像访问局部变量一样访问document及该元素本身的成员。这个函数使用with像下面这样扩展作用域:

function () {
    with(document) {
        with(this) {
            // 元素属性值
        }
    }
}

如此一来,事件处理程序要访问自己的属性就简单多了。下面这行代码与前面的例子效果相同:

<input type='button' value='Click Me' onclick='alert(value)'>

如果当前元素是一个表单输入元素,则作用域中还会包含访问表单元素的入口,这个函数就变成了如下所示:

<form method='post'>
    <input type='text' name='username' value=''>
    <input type='button' value='Echo Username' onclick='alert(username.value)'>
</form>

在这个例子中,单击按钮会显示文本框中的文本。值得注意的是,这里直接引用了username元素。
不过,在HTML中指定事件处理程序有两个缺点。

  • 存在一个执行顺序的问题。因为用户可能会在HTML元素一出现在页面上就触发相应的事件,但当时的事件处理程序有可能尚不具备执行条件。以前面的例子来说明。
<input type='button' value='Click Me' onclick='showMessage()' />

<script type='text/javascript'>
    function showMessage() {
        alert('hello world');
    }
</script>

如果用户在页面解析showMessage()函数之前就单击了按钮,就会引发错误。为此,很多HTML事件处理程序都会被封装在一个try-catch块中,以便错误不会浮出水面。

<input type='button' value='Click Me' onclick='try{showMessage()}catch(ex){}'>
  • 另一个缺点就是,这样扩展事件处理程序的作用域链在不同浏览器中会导致不同结果。不同Javascript引擎遵循的标识符解析规则略有差异,很可能会在访问非限定对象成员时出错。

  • 最后一个缺点是HTML与Javascript代码紧密耦合。如果要更换事件处理程序,就要改动两个地方:HTML代码和Javascript代码。

DOM0级事件处理程序

通过JavaScript指定事件处理程序的传统方式,就是讲一个函数赋值给一个事件处理程序属性。这种为事件处理程序赋值的方法是在第四代Web浏览器中出现的,而且至今仍然为所有现代浏览器所支持。原因:

  • 简单
  • 跨浏览器的优势

每个元素都有自己的事件处理程序属性,这些属性通常全部小写,例如onclick。将这种属性的值设置为一个函数,就可以指定事件处理程序:

var btn = document.getElementById('myBtn');

btn.onclick = function () {
    alert('Clicked');
}

有一些缺点:
在这些代码运行以前不会指定事件处理程序,因此如果这些代码在页面中位于按钮后面,就有可能在一段时间内怎么单击都没反应。

使用DOM0级方法指定的事件处理程序被认为是元素的方法。因此,这时候的事件处理程序是在元素的作用域中运行;换句话说,程序中的this引用当前元素。

<input type='button' id='myBtn'>

var btn = document.getElementById('myBtn');

btn.onclick = function () {
    alert(this.id);
}

单击按钮显示的是元素的ID,这个ID是通过this.id取得的。不仅仅是ID,实际上可以在事件处理程序中通过this访问元素的任何属性和方法。以这种方式添加的事件处理程序会在事件流的冒泡阶段被处理。
也可以删除通过DOM0级方法指定的事件处理程序,只要像下面这样将事件处理程序属性的值设置为null即可:

btn.onclick = null;

DOM2级事件处理程序

“DOM2级事件”定义了两个方法,用于处理指定和删除事件处理程序的操作:addEventListener()和removeEventListener()。所有DOM节点中都包含这两个方法,并且它们都接收3个参数:要处理的事件名、作为事件处理程序的函数和一个布尔值。最后这个布尔值参数如果是true,表示在捕获阶段调用事件处理程序;如果是false,表示在冒泡阶段调用事件处理程序。

要在按钮上为click事件添加事件处理程序,可以使用下列代码:

var btn = document.getElementById('myBtn');

btn.addEventListener('click', function () {
    alert(this.id);
}, false);

上面的代码为一个按钮添加了onclick事件处理程序,而且该事件会在冒泡阶段被触发。与DOM0级方法一样,这里添加的事件处理程序也是在其依附的元素的作用域中运行。使用DOM2级方法添加事件处理程序的好处是可以添加多个事件处理程序。来看下面的例子。

var btn = document.getElementById('MyBtn');

btn.addEventListener('click', function () {
    alert('Hello world');
}, false);

通过addEventListener()添加的事件处理程序只能使用removeEventListener()来移除;移除时传入的参数与添加处理程序时使用的参数相同。这也意味着通过addEventListener()添加的匿名函数将无法移除

var btn = document.getElementById('myBtn');

btn.addEventListener('click', function () {
    alert(this.id);
}, false)

// 这里省略了其他代码
btn.removeEventListener('click', function () {
    alert(this.id);
}, false)  // 这样无效


var handler = function ()  {
    alert(this.id);
}

btn.addEventListener('click', handler, false)


btn.removeEventListener('click', handler, false)  // 这样有效

大多数情况下,都是将事件处理程序添加到事件流的冒泡阶段,这样可以最大限度地兼容各种浏览器。最好只在需要在事件到达目标之前截获它的时候将事件处理程序添加到捕获阶段。如果不是特别需要,我们不建议在事件捕获阶段注册事件处理程序。

IE事件处理程序(如果放弃IE8及以下可以略过)

IE实现了与DOM中类似的两个方法:attachEvent()和detachEvent()。这两个方法接收相同的两个参数:事件处理程序与事件处理程序函数。由于IE8及更早版本只支持事件冒泡,所以通过attachEvent()添加的事件处理程序都会被添加到冒泡阶段。

要使用attachEvent()为按钮添加一个事件处理程序,可以使用以下代码。

var btn = docunment.getElementById('myBtn');
btn.attachEvent('onclick', function () {
    alert('Clicked');
})

在IE中使用attachEvent()与使用DOM0级方法的主要区别在于事件处理程序的作用域。在使用DOM0级方法的情况下,事件处理程序会在全局作用域运行,因此this等于window。

来看下面的例子。

var btn = document.getElementById('myBtn');
btn.attachEvent('onclick', function () {
    alert(this=== window);  // true
})

在编写跨浏览器的代码时,牢记这一区别非常重要。

使用attachEvent()添加的事件可以通过detachEvent()来移除,条件是必须提供相同的参数。与DOM方法一样,这也意味着添加的匿名函数将不能被移除。不过,只要能够将对相同函数的引用传给dtachEvent(),就可以移除相应的事件处理程序。

跨浏览器的事件处理程序

为了以跨浏览器的方式处理事件,不少开发人员会使用能够隔离浏览器差异的JavaScript库。

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.detach('on'+ type, handler);
        } else {
            element['on' + type] = null;
        }
    }
}

事件对象

在触发DOM上的某个事件时,会产生一个事件对象event,这个对象中包含着所有与事件有关的信息。包括导致事件的元素、事件的类型以及其他与特定事件相关的信息。

DOM中的事件对象

兼容DOM的浏览器会将一个event对象传入到事件处理程序中。无论指定事件处理程序时使用什么方法(DOM0级或DOM2级),都会传入event对象。

var btn = document.getElementById('myBtn');

btn.onclick = function (event) {
    alert(event.type)  //'click'
}

btn.addEventListener('click', function (event) {
    alert(event.type)   //'click'
}, false);

这个例子中的两个事件处理程序都会弹出一个警告框,显示由event.type属性表示的事件类型。这个属性始终都会包含被触发的事件类型。

event对象包含与创建它的特定事件有关的属性和方法。触发的事件类型不一样,可用的属性和方法也不一样。不过,所有事件都会有下表列出的成员。

属性/方法 类型 读/写 说明
preventDefault() Function 只读 取消事件的默认行为。如果cancelable是true,则可以使用这个方法
stopImmediatePropagation() Function 只读 取消事件的进一步捕获或冒泡。同时阻止任何事件处理程序被调用(DOM3级事件中新增)
stopPropagation() Function 只读 取消事件的进一步捕获或冒泡。如果bubbles为true,则可以使用这个方法
target Element 只读 事件的目标
trusted Boolean 只读 为true表示事件是浏览器生成的。为false表示事件是由开发人员通过Javascript创建的(DOM3级事件中新增)
type String 只读 被触发的事件的类型
view AbstractView 只读 与事件关联的抽象视图。等同于发生事件的window对象

看下面例子:

document.body.onclick = function (event) {
    alert(event.currentTarget === document.body);  // true
    alert(this === document.body) // true
    alert(event.target === document.getElementById('myBtn')); // true
};

当单击这个例子中的按钮时,this和currentTarget都等于document.body,因为事件处理程序是注册到这个元素上的。然而,target元素却等于按钮元素,因为它是click事件真正的目标。由于按钮上并没有注册事件处理程序,结果click事件就冒泡到document.body,在那里事件才得到了处理。

在需要通过一个函数处理多个事件时,可以使用type属性。例如:

var btn = document.getElementById('myBtn');

var handler = function (event) {
    switch (event.type) {
        case 'click':
            alert('Clicked');
            break;
        
        case 'mouseover':
            event.target.style.backgroundColor = 'red';
            break;
            
        case 'mouseout':
            event.target.style.backgroundColor = '';
            break;
    }
};

btn.onclick = handler;
btn.onmouseover = handler;
btn.onmouseout = handler;

要阻止特定事件的默认行为,可以使用preventDefault()方法。例如,链接的默认行为就是在被单击时会导航到其href特性指定的URL。如果你想阻止链接导航这一默认行为,那么通过链接onclick事件处理程序可以取消它。

只有cancelable属性设置为true的事件,才可以使用preventDefault()来取消其默认行为。

另外,stopPropagation()方法用于立即停止事件在DOM层次中的传播,即取消进一步的事件捕获或冒泡。例如,直接添加到一个按钮的事件处理程序可以调用stopPropagation(),从而避免触发注册在document.body上面的事件处理程序,如下面的例子所示。

事件对象的eventPhase属性,可以用来确定事件当前正位于事件流的哪个阶段。如果是在捕获阶段调用的事件处理程序,eventPhase等于1;如果事件处理程序处于目标对象上,则eventPhase等于2;如果在冒泡阶段调用的事件处理程序,eventPhase等于3。
注意,尽管“处于目标”发生在冒泡阶段,但eventPhase仍然一直等于2。

当eventPhase等于2时,this、target和currentTarget始终是相等的。

只有在事件处理程序执行期间,event对象才会存在;一旦事件处理程序执行完成,event对象就会被销毁。

IE中的事件对象

与访问DOM中的event对象不同,要访问IE中的event对象有几种不同的方式,取决于指定事件处理程序的方法。在使用DOM0级方法添加事件处理程序时,event对象作为window对象的一个属性存在。来看下面的例子。

var btn = document.getElementById('myBtn');
btn.onclick = function () {
    var event = window.event;
    alert(event.type)  //'click'
}

可是,如果事件处理程序是使用attachEvent()添加的,那么就会有一个event对象作为参数被传入事件处理程序函数中,如下所示。

var btn = document.getElementById('myBtn');
btn.attacehEvent('onclick', function (event) {
    alert(event.type);  //'click'
})

在像这样使用attachEvent()情况下,也可以通过window对象来访问event对象,就像使用DOM0级方法时一样。不过为方便起见,同一个对象也会作为参数传递。

如果是通过HTML特性指定的事件处理程序,那么还可以通过一个名叫event的变量来访问event对象(与DOM中的事件模型相同)。

<input type='button' value='Click Me' onclick='alert(event.type)' >

IE的event对象同样也包含与创建它的事件相关的属性和方法。其中很多属性和方法都有对应的或者相关的DOM属性和方法。与DOM的event对象一样,这些属性和方法也会因为事件类型的不同而不同,但所有事件对象都会包含下表所列的属性和方法。

属性/方法 类型 读/写 说明
cancelBubble Boolean 读/写 默认值false,但将其设置为true就可以取消事件冒泡(与DOM中的stopPropagation()方法的作用相同)
returnValue Boolean 读/写 默认值为true,将其设置为false就可以取消事件的默认行为(与DOM中的preventDefault()方法的作用相同)
srcElement Element 只读 事件的目标(与DOM中的target属性相同)
type String 只读 被触发的事件的类型

因为事件处理程序的作用域是根据指定它的方式来确定的,所以不能认为this会始终等于事件目标。故而,最好还是使用event.srcElement比较保险。

跨浏览器的事件对象

虽然DOM和IE中的event对象不同,但基于它们之间的相似性依旧可以拿出跨浏览器的方案来。IE中event对象的全部信息和方法DOM对象中都有,只不过实现方式不一样。不过,这种对应关系让实现两种事件模型之间的映射非常容易。可以对前面介绍的EventUtil对象加以增强,添加如下方法以求同存异。

var EventUtil = {
    addHandler: function (element, type, handler) {},
    removeHandler: function (element, type, handler) {},
    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;
        }
    }
}

跨浏览器阻止事件冒泡:

var btn = document.getElementById('myBtn');

btn.onclick = function (event) {
    alert('Clicked');
    
    event = EventUtil.getEvent(event);
    EventUtil.stopPropagation(event);
}

document.body.onclick = function (event) {
    alert('Body clicked');
}

事件类型

Web浏览器中可能发生的事件有很多类型。

“DOM3级事件”规定了以下几类事件。

  • UI事件,当用户与页面上的元素交互时触发。
  • 焦点事件,当元素获得或失去焦点时触发。
  • 鼠标事件,当用户通过鼠标在页面上执行操作时触发。
  • 滚轮事件
  • 文本事件
  • 键盘事件
  • 合成事件,**当为IME(Input Method Editor)输入字符时触发
  • 变动(mutation)事件,当底层DOM结构发生变化时触发
  • 变动名称事件,当元素或属性名变动时触发。此类事件已经被废弃,没有任何浏览器实现它们

UI事件

UI事件指的是那些不一定与用户操作有关的事件。这些事件在DOM规范出现之前,都是以这个或那种形式存在的,而在DOM规范中保留是为了向后兼容。
现有的UI事件如下:

  • DOMActivate:表示元素已经被用户操作激活。
  • load:当页面完全加载后在window上面触发,当所有框架都加载完毕时在框架集上面触发,当图像加载完毕时在元素上触发,或者当嵌入的内容加载完毕时在元素上面触发。
  • unload:当页面完全卸载后在window上面触发,当所有框架都卸载后在框架集上面触发,或者当嵌入的内容卸载完毕后在元素上面触发。
  • abort:在用户停止下载过程时,如果嵌入的内容没有加载完,则在元素上面触发。
  • error:当发生Javascript错误时在window上面触发,当无法加载图像时在元素上面触发,当无法加载嵌入内容时在元素上面触发,或者当有一或多个框架无法加载时在框架集上面触发。
  • select:当用户选择文本框(<input>或<textarea>)中的一或多个字符时触发。
  • resize:当窗口或框架的大小变化时在window或框架上面触发。
  • scroll:当用户滚动带滚动条的元素中的内容时,在该元素上面触发。元素中包含所加载页面的滚动条。
  • 多数这些事件都与window对象或表单控件相关。

    要确定浏览器是否支持DOM2级事件规定的HTML事件,可以使用如下代码:

    var isSupported = document.implementation.hasFeature('HTMLEvents', '2.0');
    

    load事件

    最常用的一个事件。当页面完全加载后(包括所有图像、JavaScript文件、CSS文件等外部资源),就会触发window上面的load事件。

    有两种定义onload事件处理程序的方式。
    第一种方式是使用如下所示的JavaScript代码:

    EventUtil.addHandler(window, 'load', function (event) {
        alert('Loaded!');
    })
    

    第二种指定onload事件处理程序的方式是为<body>元素添加一个onload特性:

    <!DOCTYPE html>
    <html>
    <head>
        <title>Load Event Example</title>
    </head>
    <body onload='alert("loaded!")'>
    </body>
    </html>
    

    一般来说,在window上面发生的任何事件都可以在<body/>元素中通过相应的特性来指定,因为在HTML中无法访问window元素。实际上,这只是为了保证向后兼容的一种权宜之计。建议尽可能使用javascript方式。

    图像上面也可以触发load事件,无论是在DOM中的图像元素还是HTML中的图像元素。因此,可以在HTML中为任何图像指定onload事件处理程序:

    <img src='smile.gif' onload='alert("Image loaded.")'>
    

    这样,当例子中的图像加载完毕后就会显示一个警告框。同样的功能可以使用JavaScript实现:

    var $image = document.getElementById('myImg');
    $image.addEventListener('load', function (e) {
        alert('image loaded');
    })
    

    还有一些元素也以非标准的方式支持load事件。在IE9+、Firefox、Opera、Chrome和Safari 3+及更高版本中,<script>元素也会触发load事件,以便开发人员确定动态加载的JavaScript文件是否加载完毕。与图像不同,只有设置了<script>元素的src属性并将元素添加到文档后,才会开始下载JavaScript文件。换句话说,对于<script>元素而言,指定src属性和指定事件处理程序的先后顺序就不重要了。

    unload事件

    与load事件对应的是unload事件,这个事件在文档被完全卸载后触发。只要用户从一个页面切换到另一个页面,就会发生unload事件。而利用这个事件最懂的情况是清除引用,以避免内存泄漏。

    **无论使用那种方式,都要小心编写onunload事件处理程序中的代码。既然unload事件是在一切都被卸载之后才触发,那么在页面加载后存在的那些对象,此时就不一定存在了。此时,操作DOM节点或者元素的样式就会导致错误。

    resize事件

    当浏览器窗口被调整到一个新的高度或宽度时,就会触发resize事件。

    关于何时会触发resize事件,不同浏览器有不同的机制。IE、Safari、Chrome和Opera会在浏览器窗口变化了1像素就触发resize事件,然后随着变化不断重复触发。Firefox则只会在用户停止调整窗口大小时才会触发resize事件。

    scroll事件

    虽然scroll事件是在window对象上发生的,但它实际表示的则是页面中相应元素的变化。
    **在混杂模式下,可以通过<body>元素的scrollLeft和scrollTop来监控到这一变化;而在标准模式下,除Safari之外的所有浏览器都会通过<html>元素来反映这一变化(Safari仍然基于<body>跟踪滚动位置)。 **

    与resize事件类似,scroll事件也会在文档被滚动期间重复被触发,所以有必要尽量保持事件处理程序的代码简单。

    焦点事件

    焦点事件会在页面获得或失去焦点时触发。利用这些事件并与document.hasFocus()方法及document.activeElement属性配合,可以知晓用户在页面上的行踪,有以下6个焦点事件。

    • blur:在元素失去焦点时触发。这个事件不会冒泡;所有浏览器都支持它。
    • DOMFocusIn: 只有opera支持
    • DOMFocusOut: 只有opera支持
    • focus:在元素获得焦点时触发,不会冒泡
    • focusin:在元素获得焦点时触发,与focus等价,但是会冒泡
    • focusout: 在元素失去焦点时触发

    这一类事件中最主要的两个是focus和blur,它们都是javscript早期就得到所有浏览器支持的事件。这些事件的最大问题是不冒泡。因此,IE的focusin和focusout与Opera的DOMFocusIn和DOMFocusOut才会重叠。IE的方式最后被DOM3级事件采纳为标准方式。

    当焦点从页面中的一个元素移动到另一个元素,会依次触发下列事件:

    • focusout在失去焦点的元素上触发
    • focusin在获得焦点的元素上触发
    • blur在失去焦点的元素上触发
    • DOMFocusOut在失去焦点的元素上触发
    • focus在获得焦点的元素上触发
    • DOMFocusIn在获得焦点的元素上触发 其中,blur、DOMFocusOut和focusout的事件目标是失去焦点的元素;而focus、DOMFocusIn和focusin的事件目标是获得焦点的元素。

    即使focus和blur不冒泡,也可以在捕获阶段侦听它们。

    下面演示全局侦听blur和focus

    鼠标与滚轮事件

    鼠标事件是Web开发中最常用的一类事件。DOM3级事件中定义了9个鼠标事件。

    • click 略
    • dblclick 略
    • mousedown 略
    • mouseenter,在鼠标光标从元素外部首次移动到元素范围之内时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。IE、Firefox 9+和Opera支持这个事件。
    • mouseleave,在位于元素上方的鼠标光标移动到元素范围之外时触发。这个事件不冒泡,而且在光标移动到后代元素上不会触发。DOM2级事件并没有定义这个事件,但DOM3级事件将它纳入了规范。IE、Firefox 9+和Opera支持这个事件。
    • mousemove,当鼠标指针在元素内部移动时重复地触发。不能通过键盘触发这个事件。
    • mouseout,在鼠标指针位于一个元素上方,然后用户将其移入另一个元素时触发。又移入的另一个元素可能位于前一个元素的外部,也可能是这个元素的子元素。不能通过键盘触发这个事件。
    • mouseover,在鼠标指针位于一个元素外部,然后用户将其首次移入另一个元素边界之内时触发。
    • mouseup,在用户释放鼠标按钮时触发。不能通过键盘触发这个事件。

    页面中的所有元素都支持鼠标事件。除了mouseenter和mouseleave,所有鼠标事件都会冒泡,也可以被取消,而取消鼠标事件将会影响浏览器的默认行为。取消鼠标事件的默认行为还会影响其他事件,因为鼠标事件与其他事件是密不可分的关系。

    **只有在同一个元素上相继触发mousedown和mouseup事件,才会触发click事件;如果mousedown或mouseup中的一个被取消,就不会触发click事件。类似地,只有触发两次click事件,才会触发一次dblclick事件。如果有代码阻止了连续两次触发click事件,那么就不会触发dblclick事件了。

    这4个事件触发的顺序始终如下:

    • mousedown
    • mouseup
    • click
    • mousedown
    • mouseup
    • click
    • dblclick

    使用以下代码可以检测浏览器是否支持以上DOM2级事件(除dbclick、mouseenter和mouseleave之外):

    var isSupported = document.implementation.hasFeature('MouseEvents', '2.0');
    

    要检测浏览器是否支持上面的所有事件,可以使用以下代码:

    var isSupported = document.implementation.hasFeature('MouseEvents', '3.0');
    

    鼠标事件中还有一类滚轮事件。而说是一类事件,其实就是一个mousewheel事件。这个事件跟踪鼠标滚轮,类似于Mac的触控板。

    客户区坐标

    鼠标事件都是在浏览器视口中特定的位置上发生的。这个位置信息保存在事件对象的clientX和clientY属性中。所有浏览器都支持这两个属性。

    页面坐标位置

    通过客户区坐标能够知道鼠标是在视口中什么位置发生的,而页面坐标通过事件对象的pageX和pageY属性,能告诉你事件是在页面中的什么位置发生的。换句话说,这两个属性表示鼠标光标在页面中的位置,因此坐标是从页面本身而非视口的左边和顶边计算的。

    屏幕坐标位置

    鼠标事件发生时,不仅会有相对于浏览器窗口的位置,还有一个相对于整个电脑屏幕的位置。而通过screenX和screenY属性就可以确定鼠标事件发生时鼠标指针相对于整个屏幕的坐标信息。

    修改键

    虽然鼠标事件主要是使用鼠标来触发的,但在按下了鼠标时键盘上的某些键的状态也可以影响到所要采取的操作。这些修改键就是Shift、Ctrl、Alt和Meta,它们经常被用来修改鼠标事件的行为。DOM为此规定了4个属性,表示这些修改键的状态:shiftKey、ctrlKey、altKey和metaKey。

    相关元素

    在发生mouseover和mouseout事件时,还会涉及更多的元素。这两个事件都涉及把鼠标指针从一个元素的边界之内移动到另一个元素的边界之内。对mouseover事件而言,事件的主目标是获得光标的元素,而相关元素就是那个失去光标的元素。类似的,对mouseout事件而言,事件的主目标是失去光标的元素,而相关元素则是获得光标的元素。

    DOM通过event对象的relatdTarget属性提供了相关元素的信息。这个属性只对于mouseover和mouseout事件才包含值;对于其他事件,这个属性的值是null。IE8及之前版本不支持relatedTarget属性,但提供了保存着同样信息的不同属性。在mouseover事件触发时,IE的fromElement属性中保存了相关元素;在mouseout事件触发时,IE的toElement属性中保存着相关元素。

    鼠标按钮

    只有在主鼠标按钮被单击时才会触发click事件,因此检测按钮的信息并不是必要的。但对于mousedown和mouseup事件来说,则在其event对象存在一个button属性,表示按下或释放的按钮。DOM的button属性可能有如下3个值:0表示主鼠标按钮,1表示中间的鼠标按钮,2表示次鼠标按钮。在常规的设置中,主鼠标按钮就是鼠标左肩,而次鼠标按钮就是鼠标右键。

    在使用onmouseup事件处理程序时,button的值表示释放的是哪个按钮。此外,如果不是按下或释放了主鼠标按钮,Opera不会触发mouseup或mousedown事件。

    更多的事件信息

    “DOM2级事件”规范在event对象中还提供了detail属性,用于给出有关事件的更多信息。对于鼠标事件来说,detail中包含了一个数值,表示在给定位置上发生了多少次单击。在同一个像素上相继地发生一次mousedown和一次mouseup事件算作一次单击。detail属性从1开始计数,每次单击发生后都会递增。如果鼠标在mousedown和mouseup之间移动了位置,则detail会被重置为0

    鼠标滚轮事件

    IE6.0首先实现了mousewheel事件。此后,Opera、Chrome和Safari也都实现了这个事件。当用户通过鼠标滚轮与页面交互、在垂直方向上滚动页面时,就会触发mousewheel事件。这个事件可以在任何元素上面触发,最终会冒泡到document或window对象。与mousewheel事件对应的event对象除包含鼠标事件的所有标准信息外,还包含一个特殊的wheelDelta属性。当用户向前滚动鼠标滚轮时,wheelDelta是120的倍数;当用户向后滚动鼠标滚轮时,wheelDelta是-120倍数。

    触摸设备

    IOS和android设备的实现非常特别,因为这些设备没有鼠标。在面向iPhone和iPod中的Safari开发时,要记住以下几点:

    • 不支持dblclick事件。双击浏览器窗口会放大画面,而且没有办法改变该行为
    • 轻击可单击元素会触发mousemove事件。如果此操作会导致内容变化,将不再有其他事件发生;如果屏幕没有因此变化,那么会依次发生mousedown、mouseup和click事件。轻击不可单击的元素不会触发任何事件。可单击的元素是指那些单击可产生默认操作的元素(如链接),或者那些已经被指定了onclick事件处理程序的元素。
    • 两个手指放在屏幕上且页面随手指移动而滚动时会触发mousewheel和scroll事件。

    键盘与文本事件

    用户在使用键盘时会触发键盘事件。“DOM2级事件”最初规定了键盘事件,但在最终定稿之前又删除了相应的内容。结果,对键盘事件的支持主要遵循的是DOM0级。

    “DOM3级事件”为键盘事件指定了规范,IE9率先完全实现了该规范。其他浏览器也在着手实现这一标准,但仍然有很多问题。

    • keydown:当用户按下键盘上的任意键时触发,而且如果按住不放的话,会重复触发。
    • keypress: 当用户按下键盘上的字符键时触发,而且如果按住不放的话,会重复触发此事件。
    • keyup: 当用户释放键盘上的键时触发。
      虽然所有元素都支持以上3个事件,但只有在用户通过文本框输入文本时才最常用到。

    只有一个文本事件:textInput。这个事件是对keypress的补充,用意是在将文本显示给用户之前更容易拦截文本。在文本插入文本框之前会触发textInput事件。

    在用户按了一下键盘上的字符键时,首先会触发keydown事件,然后紧跟着是keypress事件,最后会触发keyup事件。其中,keydown和keypress都是在文本框发生变化之前被触发的;而keyup事件则是在文本框已经发生变化之后被触发的。如果用户按下了一个字符键不放,就会重复触发keydown和keypress事件,直到用户松开该键为止。

    如果用户按下的是一个非字符键,那么首先会触发keydown事件,然后是keyup事件。如果按着不放,就会一直重复触发keydown事件,直到用户松开这个键,此时会触发keyup事件。

    键盘事件与鼠标事件一样,都支持相同的修改键。而且,键盘事件的事件对象中也有shiftKey、ctrlKey、altKey和metaKey属性。IE不支持metaKey。

    键码

    在发生keydown和keyup事件时,event对象的keyCode属性中会包含一个代码,与键盘上一个特定的键对应。对数字字母字符键,keyCode属性的值与ASCII码中对应下泄字母或数字的编码相同。

    因此,数字键7的keyCode值为55,而字母A键的keyCode值为65 ———— 与Shift键的状态无关。

    字符编码

    发生keypress事件意味着按下的键会影响到屏幕中文本的显示。在所有浏览器中,按下能够插入或删除字符的键都会触发keypress事件;按下其他键能否触发此事件因浏览器而异。

    DOM3级变化

    尽管所有浏览器都实现了某种形式的键盘事件,DOM3级事件还是做出了一些改变。比如,DOM3级事件中的键盘事件,不再包含charCode属性,而是包含两个新属性:key和char。

    其中,key属性是为了取代keyCode而新增的,它的值是一个字符串。在按下某个字符键时,key的值就是相应的文本字符(如k或M);在按下非字符键时,key的值是相应键的名(如Shift或Down)。而char属性在按下字符键时的行为与key相同,但在按下非字符键时值为null。
    DOM3级事件还添加了一个名为location的属性,这是一个数值,表示按下了什么位置上的键:

    • 0表示默认键盘
    • 1表示左侧位置(如左位的Alt键)
    • 2表示右侧位置(右侧的Shift键)
    • 3表示数字小键盘
    • 4表示移动设备键盘(也就是虚拟键盘)
    • 5表示手柄

    textInput事件

    “DOM3级事件”规范中引入了一个新事件,名叫textInput。根据规范,当用户在可编辑区域中输入字符时,就会触发这个事件。这个用于替代keypress的textInput事件的行为稍有不同。

    • 区别之一就是任何可以获得焦点的元素都可以触发keypress事件,但只有可编辑区域才能触发textInput事件。
    • 区别之二是textInput事件只会在用户按下能够输入实际字符的键时才会被触发,而keypress事件则在按下那些能够影响文本显示的键时也会触发(如退格键)。

    由于textInput事件主要考虑的是字符,因此它的event对象中包含一个data属性,这个属性的值就是用户输入的字符(而非字符编码)。换句话说,用户在没有按上档键的情况下按下了S键,data的值就是‘s',而如果在按住上档键时按下该键,data的值就是'S'。

    设备中的键盘事件

    任天堂Wii会在用户按下Wii遥控器上的按键时触发键盘事件。

    复合事件(composition event)

    复合事件是DOM3级事件中新添加的一类事件,用于处理IME的输入序列。
    IME(Input Method Editor,输入法编辑器)可以让用户输入在物理键盘上找不到的字符。例如,使用拉丁文键盘的用户通过IME照样能输入日文字符。IME通常需要同时按住多个键,但最终只输入一个字符。复合事件就是针对检测和处理这种输入而设计的。

    • compositionstart: 在IME的输入法打开时触发,表示要开始输入了。
    • compositionupdate: 在向输入字段中插入新字符时触发。
    • compositionend:在IME的输入法关闭时触发,表示返回正常键盘输入状态。

    复合事件与文本事件有很多方面都很相似。在触发复合事件时,目标是接收文本的输入字段。但它比文本事件的事件对象多一个属性data,其中包含以下几个值中的一个:

    • 如果在compositionstart事件发生访问,包含正在编辑的文本。
    • 如果在compositionupdate事件发生时访问,包含正插入的新字符。
    • 如果在compositionend事件发生时访问,包含此次输入会话中插入的所有字符。

    利用监听compositionstart判断是否开启了输入法。从而实现体验较为良好兼容性较强的监控字数的控件。

    变动事件

    DOM2级的变动(mutation)事件能在DOM中的某一部分发生变化时给出提示。变动事件是为XML或HTML DOM设计的,并不特定于某种语言。DOM2级定义了如下变动事件。

    • DOMSubtreeModified:在DOM结构中发生任何变化时触发。这个事件在其他任何事件触发后都会触发
    • DOMNodeInserted:在一个节点作为子节点被插入到另一个节点中时触发。
    • DOMNodeRemoved:在节点从其父节点中被移除时触发。
    • DOMNodeInertedIntoDocument:在一个节点被直接插入文档或通过子树间接插入文档之后触发。这个事件在DOMNodeInserted之后触发。
    • DOMNodeRemovedFromDocument:在一个节点被直接从文档中移除或通过子树间接从文档中移除之前触发。
    • DOMAttrModified:在特性被修改之后触发。
    • DOMCharacterDataModified:在文本节点的值发生变化时触发。

    使用下列代码可以检测出浏览器是否支持变动事件:

    var isSupported = document.implementation.hasFeature('MutationEvents', '2.0');
    

    删除节点时的触发顺序

    • 在使用removeChild()或replaceChild()从DOM中删除节点时,首先会触发DOMNodeRemoved事件。这个事件的目标(event.target)是被删除的节点,而event.relatedNode属性中包含着对目标节点父节点的引用。在这个事件触发时,节点尚未从其父节点删除,因此其parentNode属性仍然指向父节点。

    • 如果被移除的节点包含子节点,那么在其所有子节点以及这个被移除的节点上会相继触发DOMNodeRemovedFromDocument事件。但这个事件不会冒泡,所以只有直接指定给其中一个子节点的事件处理程序才会被调用。这个事件的目标是相应的子节点或者那个被移除的节点,除此之外event对象中不包含其他信息。

    • 紧随其后触发的是DOMSubtreeModified事件。这个事件的目标是被移除节点的父节点;此时的event对象也不会提供与事件相关的其他信息。

    插入节点时的触发顺序

    • 在使用appendChild()、replaceChild()或insertBefore()向DOM中插入节点时,首先会触发DOMNodeInserted事件。这个事件的目标是被插入的节点,而event.relatedNode属性中包含一个对父节点的引用。在这个事件触发时,节点已经被插入到了新的父节点中。这个事件是冒泡的,因此可以在DOM的各个层次上处理它。
    • 紧接着,会在新插入的节点上面触发DOMNodeInsertedIntoDocument事件。这个事件不冒泡,因此必须在插入节点之前为它添加这个事件处理程序。这个事件的目标是被插入的节点,除此之外event对象中不包含其他信息。
    • 最后一个触发的事件是DOMSubtreeModified,触发于新插入节点的父节点。

    HTML5事件

    DOM规范没有涵盖所有浏览器支持的所有事件。HTML5详尽列出了浏览器应该支持的所有事件。本节只讨论其中得到浏览器完善支持的事件,但并非全部事件。

    contextmenu事件

    contextmenu这个事件,用以表示何时应该显示上下文菜单,以便开发人员取消默认的上下文菜单而提供自定义菜单。

    由于contextmenu事件是冒泡的,因此可以为document指定一个事件处理程序,用以处理页面中发生的所有此类事件。这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件。

    这个事件的目标是发生用户操作的元素。在所有浏览器中都可以取消这个事件:在兼容DOM的浏览器中,使用event.preventDefault();在IE中,将event.returnValue()的值,设置为false。因为contextmenu事件属于鼠标事件,所以其事件对象中包含与光标位置有关的所有属性。通常使用contextmenu事件来显示自定义的上下文菜单,而使用onclick事件处理程序来隐藏该菜单。

    beforeunload事件

    之所以发生在window对象上的beforeunload事件,是为了让开发人员有可能在页面卸载前阻止这一操作。这个事件会在浏览器卸载页面之前触发,可以通过它来取消卸载并继续使用原有页面。但是,不能彻底取消这个事件,因为那就相当于让用户无法离开当前页面了。为此,这个事件的意图是将控制权交给用户。

    DOMContentLoaded事件

    window的load事件会在页面中的一切都加载完毕时触发,但这个过程可能会因为要加载的外部资源过多而破费周折。而DOMContentLoaded事件则在形成完整的DOM树之后就会触发,不理会图像、JavaScript文件、CSS文件或其他资源是否已经下载完毕。

    DOMContentLoaded事件对象不会提供任何额外的信息(其target属性是document)。

    **对于不支持DOMContentLoaded的浏览器,我们建议在页面加载期间设置一个时间为0毫秒的超时调用:

    setTimeout(function () {
        // 在此添加事件处理程序
    }, 0);
    

    这个代码的实际意思就是:"在当前JavaScript处理完成后立即运行这个函数。"在页面下载和构建期间,只有一个JavaScript处理过程,因此超时调用会在该过程结束时立即触发。为了确保这个方法有效,必须将其作为页面中的第一个超时调用;即便如此,也还是无法保证在所有环境中该超时调用一定会早于load事件被触发。

    readystatechange事件

    IE为DOM文档中的某些部分提供了readystatechange事件。这个事件的目的是提供与文档或元素的加载状态有关的信息。但这个事件的行为有时候也很难预料。支持readystatechange事件的每个对象都有一个readystate属性,可能有下列5个值中的一个:

    • uninitialized 未初始化: 对象存在但尚未初始化
    • loading 正在加载: 对象正在加载数据
    • loaded 加载完毕: 对象加载数据完成
    • interactive 交互: 可以操作对象了,但还没有完全加载
    • complete 完成: 对象已经加载完毕了

    这些状态看起来很直观,但并非所有对象都会经历readyState的这几个阶段。换句话说,如果某个阶段不适用某个对象,则该对象完全可能跳过该阶段;并没有规定哪个阶段适用于哪个对象。显然,这意味着readystatechange事件经常会少于4次,而readyState属性的值也不总是连续。

    对于document而言,值为“interactive”的readyState会在与DOMContentLoaded大致相同的时刻触发readystatechange事件。此时,DOM树已经加载完毕,可以安全地操作它了,因此就会进入交互interactive阶段。

    这个事件的event对象不会提供任何信息,也没有目标对象。

    在与load事件一起使用时,无法预测两个事件触发的先后顺序。在包含较多或较大的外部资源的页面中,会在load事件触发之前先进入交互阶段;而在包含较少或较小的外部资源的页面中,则很难说readystatechange事件会发生在load事件前面。

    让问题变得更复杂的是,交互阶段可能会早于也可能会晚于完成阶段出现,无法确保顺序。在包含较多外部资源的页面中,交互阶段更有可能早于完成阶段出现;而在页面中包含较少外部资源的情况下,完成阶段先于交互阶段出现的可能性更大。因此,为了尽可能抢到先机,有必要同时检测交互和完成阶段。

    var handler =  function (e) {
        if (document.readyState == 'interactive' || document.readyState == 'complete') {
            document.removeEventListener('readystatechange', handler);
            alert('Content loaded');
        }
    document.addEventListener('readystatechange', handler);
    

    pageshow和pagehide事件

    Firefox和Opera有一个特性,名叫“往返缓存”(back-forward cache),可以在用户使用浏览器的“后退”和“前进”按钮时加快页面的转换速度。这个缓存中不仅保存着页面数据,还保存了DOM和JavaScript状态;实际上是将整个页面都保存在了内存里。如果页面位于bfcache中,那么再次打开该页面时就不会触发load事件。尽管由于内存中保存了整个页面的状态,不触发load事件也不应该会导致什么问题,但为了更形象地说明bfcache行为,Firefox还是提供了一些新事件。

    第一个事件就是pageshow,这个事件在页面显示时触发,无论该页面是否来自bgcache。在重新加载的页面中,pageshow会在load事件触发后触发;而对于bfcache中的页面,pageshow会在页面状态完全恢复的那一刻触发。

    另外值得注意的是,虽然这个事件的目标是document,但必须将其事件处理程序添加到window。

    (function () {
        var pageshowCount = 0;
    
        window.addEventListener('pageshow', function (e) {
            pageshowCount++;
            notifyMe('page show: ' + pageshowCount);
        })
    })()
    

    这个例子使用了私有作用域,以防止变量showCount进入全局作用域。当页面首次加载完成时,showCount的值为0。此后,每当触发pageshow事件,showCount的值就会递增并通过警告框显示出来。如果你在离开包含以上代码的页面之后,又单击“后退”按钮返回该页面,就会看到showCount每次递增的值。这是因为该变量的状态,乃至整个页面的状态,都被保存在了内存中,当你返回这个页面时,它们的状态得到了恢复。如果你单击了浏览器的“刷新”按钮,那么showCount的值就会被重置为0,因为页面已经完全重新加载了。

    除了通常的属性之外,pageshow事件的event对象还包含了一个名为presisted的布尔值属性。如果页面被保存在了bfcache中,则这个属性的值为true;否则,这个属性的值为false。

    通过检测persisted属性,就可以根据页面在bfcache中的状态来确定是否需要采取其他操作。
    与pageshow事件对于的是pagehide事件,该事件会在浏览器卸载页面的时候触发,而且是在unload事件之前触发。与pageshow事件一样,pagehide在document上面触发,但其事件处理程序必须要添加到window对象。这个事件的event对象也包含persisted属性,不过其用途稍有不同。

    hashchange事件

    HTML5新增了hashchange事件,以便在URL的参数列表发生变化时通知开发人员。之所以新增这个事件,是因为在Ajax应用中,开发人员经常要利用URL参数列来保存状态或导航信息。

    **使用以下代码可以检测浏览器是否支持hashchange事件:

    var isSupported = ('onhashchange' in window);
    

    设备事件

    智能手机和平板电脑的普及,为用户与浏览器交互引入了一种新的方式,而一类新事件也应运而生。device event可以让开发人员确定用户在怎样使用设备。

    orientationonchange事件

    只要用户改变了设备的查看模式,就会触发orientationchange事件。此时的event对象不包含任何有价值的信息,因为唯一相关的信息可以通过window。orientation访问到。

    所有IOS设备都支持orientationchange事件和window。orientation属性。

    MozOrientation事件(已废弃)

    firefox提供。这个事件与ios中的orientationchange事件不同,该事件只能提供一个平面的方向变化。由于MozOrientation事件是在window对象上触发的,所以可以使用以下代码来处理。

    deviceorientation事件

    本质上,DeviceOrientation Event规范定义的deviceorientation事件与MozOrientation事件类似。它也是在加速计检测到设备方向变化时在window对象上触发,而且具有与MozOrientation事件相同的支持限制。
    触发deviceorientation事件时,事件对象中包含着每一轴相对于设备静止状态下发生变化的信息。事件对象包含以下5个属性。

    • alpha
    • beta
    • gamma
    • absolute
    • compassCalibrated

    devicemotion事件

    devicemotion事件。这个事件是要告诉开发人员设备什么时候移动,而不仅仅是设备方向如何改变。
    事件对象包含以下属性:

    • acceleration: 一个包含x、y和z属性的对象,在不考虑重力的情况下,告诉你在每个方向上的加速度。
    • accelerationIncludingGravity:一个包含x、y和z属性的对象,在考虑z轴自然重力加速度的情况下,告诉你在每个方向上的加速度。
    • interval:以毫秒表示的时间值,必须在另一个devicemotion事件触发前传入。这个值在每个事件中应该是一个常量。
    • rotationRate:一个包含表示方向的alpha、beta和gamma属性的对象。

    触摸和手势事件

    触摸事件

    具体来说有以下几个触摸事件:

    • touchstart:当手指触摸屏幕时触发;即使已经有一个手指放在了屏幕上也会触发。
    • touchmove:当手指在屏幕上滑动时连续地触发。在这个事件发生期间,调用preventDefault()可以阻止滚动。
    • touchend: 当手指从屏幕上移开时触发。
    • touchcancel:当系统停止跟踪触摸时触发。

    上面这几个事件都会冒泡,也都可以取消。虽然这些触摸事件没有在DOM规范中定义,但它们却是以兼容DOM的方式实现的。因此,每个触摸事件的event对象都提供了在鼠标事件中常见的属性。

    除了常见的DOM属性外,触摸事件还包含下列三个用于跟踪触摸的属性。

    • touches: 表示当前跟踪的触摸操作的Touch对象的数组
    • targetTouchs:特定于事件目标的Touch对象的数组
    • changedTouches:表示自上次触摸以来发生了什么改变的Touch对象的数组

    手势事件

    有三个手势事件,分别介绍如下。

    • gesturestart: 当一个手指已经按在屏幕上而另一个手指又触摸屏幕时触发。
    • gesturechange:当触摸屏幕的任何一个手指的位置发生变化时触发。
    • gestureend:当任何一个手指从屏幕上面移开时触发。

    只有两个手指都触摸到事件的接收容器时才会触发这些事件。在一个元素上设置事件处理程序。如果另一个手指又放在了屏幕上,则会先触发gesturestart事件,随后触发基于该手指的touchstart事件。如果一个或两个手指在屏幕上滑动,将会触发gesturechange事件。但只要有一个手指移开,就会触发gestureend事件,紧接着又会触发基于该手指的touchend事件。

    触摸事件和手势事件之间存在某种关系。当一个手指放在屏幕上时,会触发touchstart事件。如果另一个手指又放在了屏幕上,则会先触发gesturestart事件,随后触发基于该手指的touchstart事件。如果一个或两个手指在屏幕上滑动,将会触发gesturechange事件。但只要有一个手指移开,就会触发gestureend事件,紧接着又会触发基于该手指的touchend事件。

    与触摸事件一样,每个手势事件的event对象都包含着标准的鼠标事件属性:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey、metaKey。此外,还包含两个额外的属性:rotation和scale。

    其中,rotation属性表示手指变化引起的旋转角度,负值表示逆时针旋转,正值表示顺时针旋转。而scale属性表示两个手指间距离的变化情况。

    触摸事件也会返回rotation和scale属性,但这两个属性只会在两个手指与屏幕保持接触时才会发生变化。一般来说,使用基于两个手指的手势事件,要比管理触摸事件中的所有交互要容易得多。

    内存和性能

    在javascript中,添加到页面上的事件处理程序数量将直接关系到页面的整体运行性能。导致这一问题的原因是多方面。首先,每个函数都是对象,都会占用内存;内存中的对象越多,性能就越差。其次,必须事先指定所有事件处理程序而导致的DOM访问次数,会延迟整个页面的交互就绪时间。事实上,从如何利用好事件处理程序的角度出发,还是有一些方法能够提升性能的。

    事件委托

    对“事件处理程序过多”问题的解决方案就是事件委托

    在document对象添加一个事件处理程序,用以处理页面上发生的某种特定类型的事件。这样做与采取传统的做法相比具有如下优点。

    • document对象很快就可以访问,而且可以在页面生命周期的任何时点上为它添加事件处理程序(无需等待DOMContentLoaded或load事件)。换句话说,只要可单击的元素呈现在页面上,就可以立即具备适当的功能。
    • 在页面中设置事件处理程序所需的时间更少。只添加一个事件处理程序所需的DOM引用更少,所花的时间也更少。
    • 整个页面占用的内存空间更少,能够提升整体性能。

    移除事件处理程序

    每当将事件处理程序指定给元素时,运行中的浏览器代码与支持页面交互的JavaScript代码之间就会建立一个连接。这种连接越多,页面执行起来就越慢。如前所述,可以采用事件委托技术,限制建立的连接数量。另外,在不需要的时候移除事件处理程序,也是解决这个问题的一种方案。内存中留有那些过时不用的“空事件处理程序”(dangling event handler),也是造成web应用程序内存与性能问题的主要原因。

    在两种情况下。可能会造成上述问题。

    • 第一种情况就是从文档中移除带有事件处理程序的元素时。可能是使用removeChild()和replaceChild()方法时。更多的是发生在使用innerHTML替换页面中某一部分的时候。如果带有事件处理程序的元素被innerHTML删除了,那么原来添加到元素中的事件处理程序极有可能无法被当作垃圾回收。
    • 卸载页面的时候。如果在页面被卸载之前咩有清理干净事件处理程序,那它们就会滞留内存中。

    模拟事件

    事件经常由用户操作或通过其他浏览器功能来触发。但很少有人直到,也可以使用JavaScript在任意时刻来触发特定的事件,而此时的事件就如同浏览器创建的事件一样。

    DOM中的事件模拟

    参考customEvent api

    小结

    事件是将Javscript与网页联系在一起的主要方式。“DOM3级事件”规范和HTML5定义了常见的大多数事件。即使有规范定义了基本事件,但很多浏览器仍然在规范之外实现了自己的专有事件,从而为开发人员提供更多掌握用户交互的手段。

    在使用事件时,需要考虑如下一些内存与性能方面的问题。

    • 有必要限制一个页面中事件处理程序的数量,数量太多会导致占用大量内存,而且也会让用户感觉页面反应不够灵敏。
    • 建立在事件冒泡机制之上的事件委托技术,可以有效地减少事件处理程序的数量。
    • 建议在浏览器卸载页面之前移除页面中的所有事件处理程序。

    事件是JavaScript中最重要的主题之一,深入理解事件的工作机制以及它们对性能的影响至关重要。