红宝书之第十七章:事 件(中)

361 阅读15分钟

事件类型

DOM3 Events 定义了如下事件类型。

  1.  用户界面事件(UIEvent):涉及与 BOM 交互的通用浏览器事件。
  2.  焦点事件(FocusEvent):在元素获得和失去焦点时触发。
  3.  鼠标事件(MouseEvent):使用鼠标在页面上执行某些操作时触发。
  4.  滚轮事件(WheelEvent):使用鼠标滚轮(或类似设备)时触发。
  5.  输入事件(InputEvent):向文档中输入文本时触发。
  6.  键盘事件(KeyboardEvent):使用键盘在页面上执行某些操作时触发。
  7.  合成事件(CompositionEvent):在使用某种 IME(Input Method Editor,输入法编辑器)输入字符时触发。

用户界面事件

用户界面事件或 UI 事件不一定跟用户操作有关。这类事件在 DOM 规范出现之前就已经以某种形式存在了,保留它们是为了向后兼容。UI 事件主要有以下几种。

  • DOMActivate:元素被用户通过鼠标或键盘操作激活时触发(比 click 或 keydown 更通用)。这个事件在 DOM3 Events 中已经废弃。因为浏览器实现之间存在差异,所以不要使用它。
  • load:在 window 上当页面加载完成后触发,在窗套()上当所有窗格()都加载完成后触发,在元素上当图片加载完成后触发,在元素上当相应对象加载完成后触发。
  • unload:在 window 上当页面完全卸载后触发,在窗套上当所有窗格都卸载完成后触发,在元素上当相应对象卸载完成后触发。
  • abort:在元素上当相应对象加载完成前被用户提前终止下载时触发。
  • error:在 window 上当 JavaScript 报错时触发,在元素上当无法加载指定图片时触发,在元素上当无法加载相应对象时触发,在窗套上当一个或多个窗格无法完成加载时触发。
  • select:在文本框(或 textarea)上当用户选择了一个或多个字符时触发。
  • resize:在 window 或窗格上当窗口或窗格被缩放时触发。
  • scroll:当用户滚动包含滚动条的元素时在元素上触发。元素包含已加载页面的滚动条。
  • 大多数 HTML 事件与 window 对象和表单控件有关。

    除了 DOMActivate,这些事件在 DOM2 Events 中都被归为 HTML Events(DOMActivate 在 DOM2中仍旧是 UI 事件)。

    load 事件

    load 事件可能是 JavaScript 中最常用的事件。在 window 对象上,load 事件会在整个页面(包括所有外部资源如图片、JavaScript 文件和 CSS 文件)加载完成后触发。可以通过两种方式指定 load 事件处理程序。第一种是 JavaScript 方式,如下所示:

    window.addEventListener("load", (event) => { 
     console.log("Loaded!"); 
    });
    

    这是使用 addEventListener()方法来指定事件处理程序。与其他事件一样,事件处理程序会接收到一个 event 对象。这个 event 对象并没有提供关于这种类型事件的额外信息,虽然在 DOM 合规的浏览器中,event.target 会被设置为 document,但在 IE8 之前的版本中,不会设置这个对象的srcElement 属性。

    第二种指定 load 事件处理程序的方式是向元素添加 onload 属性,如下所示:

    <!DOCTYPE html> 
    <html> 
        <head> 
        	 <title>Load Event Example</title> 
        </head>
        <body onload="console.log('Loaded!')"> 
    	</body> 
    </html>
    

    一般来说,任何在 window 上发生的事件,都可以通过给元素上对应的属性赋值来指定,这是因为 HTML 中没有 window 元素。这实际上是为了保证向后兼容的一个策略,但在所有浏览器中都能得到很好的支持。实际开发中要尽量使用 JavaScript 方式。

    注意 根据 DOM2 Events,load 事件应该在 document 而非 window 上触发。可是为了向后兼容,所有浏览器都在 window 上实现了 load 事件。

    图片上也会触发load事件,包括DOM中的图片和非DOM中的图片。可以在HTML中直接给元素的 onload 属性指定事件处理程序,比如:

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

    这个例子会在图片加载完成后输出一条消息。同样,使用 JavaScript 也可以为图片指定事件处理程序:

    let image = document.getElementById("myImage"); 
    image.addEventListener("load", (event) => { 
     console.log(event.target.src); 
    });
    

    这里使用 JavaScript 为图片指定了 load 事件处理程序。处理程序会接收到 event 对象,虽然这个对象上没有多少有用的信息。这个事件的目标是元素,因此可以直接从 event.target.src 属性中取得图片地址并打印出来。

    在通过 JavaScript 创建新元素时,也可以给这个元素指定一个在加载完成后执行的事件处理程序。在这里,关键是要在赋值 src 属性前指定事件处理程序,如下所示:

    window.addEventListener("load", () => { 
         let image = document.createElement("img"); 
         image.addEventListener("load", (event) => { 
         	console.log(event.target.src); 
         }); 
         document.body.appendChild(image); 
         image.src = "smile.gif"; 
    });
    

    这个例子首先为 window 指定了一个 load 事件处理程序。因为示例涉及向 DOM 中添加新元素,所以必须确保页面已经加载完成。如果在页面加载完成之前操作 document.body,则会导致错误。然后,代码创建了一个新的元素,并为这个元素设置了 load 事件处理程序。最后,才把这个元素添加到文档中并指定了其 src 属性。注意,下载图片并不一定要把元素添加到文档,只要给它设置了 src 属性就会立即开始下载。

    同样的技术也适用于 DOM0 的 Image 对象。在 DOM 出现之前,客户端都使用 Image 对象预先加载图片。可以像使用前面(通过 createElement()方法创建)的元素一样使用 Image 对象,只是不能把后者添加到 DOM 树。下面的例子使用新 Image 对象实现了图片预加载:

    window.addEventListener("load", () => {
        let image = new Image();
        image.addEventListener("load", (event) => {
            console.log("Image loaded!");
        });
        image.src = "smile.gif";
    });
    

    这里调用 Image 构造函数创建了一个新图片,并给它设置了事件处理程序。有些浏览器会把 Image对象实现为元素,但并非所有浏览器都如此。所以最好把它们看成是两个东西。

    还有一些元素也以非标准的方式支持 load 事件。

    window.addEventListener("load", () => { 
     let script = document.createElement("script"); 
     script.addEventListener("load", (event) => { 
     	console.log("Loaded"); 
     }); 
     script.src = "example.js"; 
     document.body.appendChild(script); 
    });
    

    这里 event 对象的 target 属性在大多数浏览器中是

    IE 和 Opera 支持元素触发 load 事件,因而支持动态检测样式表是否加载完成。下面的代码展示了如何设置这样的事件处理程序:

    window.addEventListener("load", () => { 
     let link = document.createElement("link"); 
     link.type = "text/css"; 
     link.rel= "stylesheet"; 
     link.addEventListener("load", (event) => { 
     	console.log("css loaded"); 
     }); 
     link.href = "example.css"; 
     document.getElementsByTagName("head")[0].appendChild(link); 
    });
    

    unload 事件

    与 load 事件相对的是 unload 事件,unload 事件会在文档卸载完成后触发。unload 事件一般是在从一个页面导航到另一个页面时触发,最常用于清理引用,以避免内存泄漏。与 load 事件类似,unload 事件处理程序也有两种指定方式。第一种是 JavaScript 方式,如下所示:

    window.addEventListener("unload", (event) => { 
     console.log("Unloaded!"); 
    });
    

    这个事件生成的 event 对象在 DOM 合规的浏览器中只有 target 属性(值为 document)。IE8 及更早版本在这个事件上不提供 srcElement 属性。

    第二种方式与 load 事件类似,就是给元素添加 onunload 属性:

    <!DOCTYPE html> 
    <html> 
        <head> 
         	<title>Unload Event Example</title> 
        </head> 
        <body onunload="console.log('Unloaded!')"> 
        </body> 
    </html>	
    

    无论使用何种方式,都要注意事件处理程序中的代码。因为 unload 事件是在页面卸载完成后触发的,所以不能使用页面加载后才有的对象。此时要访问 DOM 或修改页面外观都会导致错误。

    注意 根据 DOM2 Events,unload 事件应该在而非 window 上触发。可是为了向后兼容,所有浏览器都在 window 上实现了 unload 事件。

    resize 事件

    当浏览器窗口被缩放到新高度或宽度时,会触发 resize 事件。这个事件在 window 上触发,因此可以通过 JavaScript 在 window 上或者为元素添加 onresize 属性来指定事件处理程序。优先使用 JavaScript 方式:

    window.addEventListener("resize", (event) => { 
     console.log("Resized"); 
    });
    

    类似于其他在 window 上发生的事件,此时会生成 event 对象,且这个对象的 target 属性在 DOM合规的浏览器中是 document。而 IE8 及更早版本中并没有提供可用的属性。

    不同浏览器在决定何时触发 resize 事件上存在重要差异。IE、Safari、Chrome 和 Opera 会在窗口缩放超过 1 像素时触发 resize 事件,然后随着用户缩放浏览器窗口不断触发。Firefox 早期版本则只在用户停止缩放浏览器窗口时触发 resize 事件。无论如何,都应该避免在这个事件处理程序中执行过多计算。否则可能由于执行过于频繁而导致浏览器响应明确变慢。

    注意 浏览器窗口在最大化和最小化时也会触发 resize 事件。

    scroll 事件

    虽然 scroll 事件发生在 window 上,但实际上反映的是页面中相应元素的变化。在混杂模式下,可以通过元素检测 scrollLeft 和 scrollTop 属性的变化。而在标准模式下,这些变化在除早期版的 Safari 之外的所有浏览器中都发生在元素上(早期版的 Safari 在上跟踪滚动位置)。下面的代码演示了如何处理这些差异:

    window.addEventListener("scroll", (event) => { 
     if (document.compatMode == "CSS1Compat") { 
     	console.log(document.documentElement.scrollTop); 
     } else { 
     	console.log(document.body.scrollTop); 
     } 
    });
    

    以上事件处理程序会在页面滚动时输出垂直方向上滚动的距离,而且适用于不同渲染模式。因为Safari 3.1 之前不支持 document.compatMode,所以早期版本会走第二个分支。

    类似于 resize,scroll 事件也会随着文档滚动而重复触发,因此最好保持事件处理程序的代码尽可能简单。

    焦点事件

    焦点事件在页面元素获得或失去焦点时触发。这些事件可以与 document.hasFocus()和document.activeElement 一起为开发者提供用户在页面中导航的信息。焦点事件有以下 6 种。

    • blur:当元素失去焦点时触发。这个事件不冒泡,所有浏览器都支持。
    • DOMFocusIn:当元素获得焦点时触发。这个事件是 focus 的冒泡版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events 废弃了 DOMFocusIn,推荐 focusin。
    • DOMFocusOut:当元素失去焦点时触发。这个事件是 blur 的通用版。Opera 是唯一支持这个事件的主流浏览器。DOM3 Events 废弃了 DOMFocusOut,推荐 focusout。
    • focus:当元素获得焦点时触发。这个事件不冒泡,所有浏览器都支持。
    • focusin:当元素获得焦点时触发。这个事件是 focus 的冒泡版。
    • focusout:当元素失去焦点时触发。这个事件是 blur 的通用版。

    焦点事件中的两个主要事件是 focus 和 blur,这两个事件在 JavaScript 早期就得到了浏览器支持。它们最大的问题是不冒泡。这导致 IE后来又增加了 focusin 和 focusout,Opera又增加了 DOMFocusIn和 DOMFocusOut。IE 新增的这两个事件已经被 DOM3 Events 标准化。

    当焦点从页面中的一个元素移到另一个元素上时,会依次发生如下事件。

    • (1) focuscout 在失去焦点的元素上触发。
    • (2) focusin 在获得焦点的元素上触发。
    • (3) blur 在失去焦点的元素上触发。
    • (4) DOMFocusOut 在失去焦点的元素上触发。
    • (5) focus 在获得焦点的元素上触发。
    • (6) DOMFocusIn 在获得焦点的元素上触发。

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

    鼠标和滚轮事件

    鼠标事件是 Web 开发中最常用的一组事件,这是因为鼠标是用户的主要定位设备。DOM3 Events定义了 9 种鼠标事件。

    • click:在用户单击鼠标主键(通常是左键)或按键盘回车键时触发。这主要是基于无障碍的考虑,让键盘和鼠标都可以触发 onclick 事件处理程序。
    • dblclick:在用户双击鼠标主键(通常是左键)时触发。这个事件不是在 DOM2 Events 中定义的,但得到了很好的支持,DOM3 Events 将其进行了标准化。
    • mousedown:在用户按下任意鼠标键时触发。这个事件不能通过键盘触发。
    • mouseenter:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。mouseenter 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events中新增的事件。
    • mouseleave:在用户把鼠标光标从元素内部移到元素外部时触发。这个事件不冒泡,也不会在光标经过后代元素时触发。mouseleave 事件不是在 DOM2 Events 中定义的,而是 DOM3 Events中新增的事件。
    • mousemove:在鼠标光标在元素上移动时反复触发。这个事件不能通过键盘触发
    • mouseout:在用户把鼠标光标从一个元素移到另一个元素上时触发。移到的元素可以是原始元素的外部元素,也可以是原始元素的子元素。这个事件不能通过键盘触发。
    • mouseover:在用户把鼠标光标从元素外部移到元素内部时触发。这个事件不能通过键盘触发。
    • mouseup:在用户释放鼠标键时触发。这个事件不能通过键盘触发。

    页面中的所有元素都支持鼠标事件。除了 mouseenter 和 mouseleave,所有鼠标事件都会冒泡,都可以被取消,而这会影响浏览器的默认行为。由于事件之间存在关系,因此取消鼠标事件的默认行为也会影响其他事件。

    比如,click 事件触发的前提是 mousedown 事件触发后,紧接着又在同一个元素上触发了 mouseup事件。如果 mousedown 和 mouseup 中的任意一个事件被取消,那么 click 事件就不会触发。类似地,两次连续的 click 事件会导致 dblclick 事件触发。只要有任何逻辑阻止了这两个 click 事件发生(比如取消其中一个 click 事件或者取消 mousedown 或 mouseup 事件中的任一个),dblclick 事件就不会发生。这 4 个事件永远会按照如下顺序触发:

    • (1) mousedown
    • (2) mouseup
    • (3) click
    • (4) mousedown
    • (5) mouseup
    • (6) click
    • (7) dblclick

    click 和 dblclick 在触发前都依赖其他事件触发,mousedown 和 mouseup 则不会受其他事件影响。

    IE8 及更早版本的实现中有个问题,这会导致双击事件跳过第二次 mousedown 和 click 事件。相应的顺序变成了:

    • (1) mousedown
    • (2) mouseup
    • (3) click
    • (4) mouseup
    • (5) dblclick

    鼠标事件在 DOM3 Events 中对应的类型是"MouseEvent",而不是"MouseEvents"。

    鼠标事件还有一个名为滚轮事件的子类别。滚轮事件只有一个事件 mousewheel,反映的是鼠标滚轮或带滚轮的类似设备上滚轮的交互。

    客户端坐标

    鼠标事件都是在浏览器视口中的某个位置上发生的。这些信息被保存在 event 对象的 clientX 和clientY 属性中。这两个属性表示事件发生时鼠标光标在视口中的坐标,所有浏览器都支持。

    可以通过下面的方式获取鼠标事件的客户端坐标:

    let div = document.getElementById("myDiv"); 
    div.addEventListener("click", (event) => { 
     console.log(`Client coordinates: ${event.clientX}, ${event.clientY}`); 
    });
    

    这个例子为

    元素指定了一个 onclick 事件处理程序。当元素被点击时,会显示事件发生时鼠标光标在客户端视口中的坐标。注意客户端坐标不考虑页面滚动,因此这两个值并不代表鼠标在页面上的位置。

    页面坐标

    客户端坐标是事件发生时鼠标光标在客户端视口中的坐标,而页面坐标是事件发生时鼠标光标在页面上的坐标,通过 event 对象的 pageX 和 pageY 可以获取。这两个属性表示鼠标光标在页面上的位置,因此反映的是光标到页面而非视口左边与上边的距离。

    可以像下面这样取得鼠标事件的页面坐标:

    let div = document.getElementById("myDiv"); 
    div.addEventListener("click", (event) => { 
     console.log(`Page coordinates: ${event.pageX}, ${event.pageY}`); 
    });
    

    在页面没有滚动时,pageX 和 pageY 与 clientX 和 clientY 的值相同。

    IE8 及更早版本没有在 event 对象上暴露页面坐标。不过,可以通过客户端坐标和滚动信息计算出来。

    屏幕坐标

    鼠标事件不仅是在浏览器窗口中发生的,也是在整个屏幕上发生的。可以通过 event 对象的screenX 和 screenY 属性获取鼠标光标在屏幕上的坐标。

    可以像下面这样获取鼠标事件的屏幕坐标:

    let div = document.getElementById("myDiv"); 
    div.addEventListener("click", (event) => { 
     console.log(`Screen coordinates: ${event.screenX}, ${event.screenY}`); 
    });
    

    与前面的例子类似,这段代码也为

    元素指定了 onclick 事件处理程序。当元素被点击时,会通过控制台打印出事件的屏幕坐标。

    修饰键

    虽然鼠标事件主要是通过鼠标触发的,但有时候要确定用户想实现的操作,还要考虑键盘按键的状态。键盘上的修饰键 Shift、Ctrl、Alt 和 Meta 经常用于修改鼠标事件的行为。DOM 规定了 4 个属性来表示这几个修饰键的状态:shiftKey、ctrlKey、altKey 和 metaKey。这几属性会在各自对应的修饰键被按下时包含布尔值 true,没有被按下时包含 false。在鼠标事件发生的,可以通过这几个属性来检测修饰键是否被按下。来看下面的例子,其中在 click 事件发生时检测了每个修饰键的状态:

    let div = document.getElementById("myDiv");
    div.addEventListener("click", (event) => {
        let keys = new Array();
        if (event.shiftKey) {
            keys.push("shift");
        }
        if (event.ctrlKey) {
            keys.push("ctrl");
        }
        if (event.altKey) {
            keys.push("alt");
        }
        if (event.metaKey) {
            keys.push("meta");
        }
        console.log("Keys: " + keys.join(","));
    });
    

    在这个例子中,onclick 事件处理程序检查了不同修饰键的状态。keys 数组中包含了在事件发生时被按下的修饰键的名称。每个对应属性为 true 的修饰键的名称都会添加到 keys 中。最后,事件处理程序会输出所有键的名称。

    注意 现代浏览器支持所有这 4 个修饰键。IE8 及更早版本不支持 metaKey 属性。

    相关元素

    对 mouseover 和 mouseout 事件而言,还存在与事件相关的其他元素。这两个事件都涉及从一个元素的边界之内把光标移到另一个元素的边界之内。对 mouseover 事件来说,事件的主要目标是获得光标的元素,相关元素是失去光标的元素。类似地,对 mouseout 事件来说,事件的主要目标是失去光标的元素,而相关元素是获得光标的元素。来看下面的例子:

    <!DOCTYPE html> 
    <html> 
        <head> 
            <title>Related Elements Example</title> 
        </head> 
        <body> 
             <div id="myDiv" 
             style="background-color:red;height:100px;width:100px;"></div> 
        </body> 
    </html>
    

    这个页面中只包含一个

    元素。如果光标开始在
    元素上,然后从它上面移出,则
    元素上会触发 mouseout 事件,相关元素为元素。与此同时,元素上会触发 mouseover事件,相关元素是
    元素。

    DOM通过 event 对象的 relatedTarget 属性提供了相关元素的信息。这个属性只有在 mouseover和 mouseout 事件发生时才包含值,其他所有事件的这个属性的值都是 null。IE8 及更早版本不支持relatedTarget 属性,但提供了其他的可以访问到相关元素的属性。在 mouseover 事件触发时,IE会提供 fromElement 属性,其中包含相关元素。而在 mouseout 事件触发时,IE 会提供 toElement属性,其中包含相关元素。(IE9 支持所有这些属性。)因此,可以在 EventUtil 中增加一个通用的获取相关属性的方法:

    var EventUtil = {
        // 其他代码
        getRelatedTarget: function (event) {
            if (event.relatedTarget) {
                return event.relatedTarget;
            } else if (event.toElement) {
                return event.toElement;
            } else if (event.fromElement) {
                return event.fromElement;
            } else {
                return null;
            }
        }
        // 其他代码
    };
    

    与前面介绍的其他跨浏览器方法一样,这个方法同样使用特性检测来确定要返回哪个值。可以像下面这样使用 EventUtil.getRelatedTarget()方法:

    let div = document.getElementById("myDiv"); 
    div.addEventListener("mouseout", (event) => { 
     let target = event.target; 
     let relatedTarget = EventUtil.getRelatedTarget(event); 
     console.log( 
     `Moused out of ${target.tagName} to ${relatedTarget.tagName}`); 
    });
    

    这个例子在

    元素上注册了 mouseout 事件处理程序。当事件触发时,就会打印出一条消息说明鼠标从哪个元素移出,移到了哪个元素上。

    鼠标按键

    只有在元素上单击鼠标主键(或按下键盘上的回车键)时 click 事件才会触发,因此按键信息并不是必需的。对 mousedown 和 mouseup 事件来说,event 对象上会有一个 button 属性,表示按下或释放的是哪个按键。DOM 为这个 button 属性定义了 3 个值:0 表示鼠标主键、1 表示鼠标中键(通常也是滚轮键)、2 表示鼠标副键。按照惯例,鼠标主键通常是左边的按键,副键通常是右边的按键。

    IE8 及更早版本也提供了 button 属性,但这个属性的值与前面说的完全不同:

    •  0,表示没有按下任何键;
    •  1,表示按下鼠标主键;
    •  2,表示按下鼠标副键;
    •  3,表示同时按下鼠标主键、副键;
    •  4,表示按下鼠标中键;
    •  5,表示同时按下鼠标主键和中键;
    •  6,表示同时按下鼠标副键和中键;
    •  7,表示同时按下 3 个键。

    很显然,DOM 定义的 button 属性比 IE 这一套更简单也更有用,毕竟同时按多个鼠标键的情况很少见。为此,实践中基本上都以 DOM 的 button 属性为准,这是因为除 IE8 及更早版本外的所有主流浏览器都原生支持。主、中、副键的定义非常明确,而 IE 定义的其他情形都可以翻译为按下其中某个键,而且优先翻译为主键。比如,IE 返回 5 或 7 时,就会对应到 DOM 的 0。

    额外事件信息

    DOM2 Events 规范在 event 对象上提供了 detail 属性,以给出关于事件的更多信息。对鼠标事件来说,detail 包含一个数值,表示在给定位置上发生了多少次单击。单击相当于在同一个像素上发生一次 mousedown 紧跟一次 mouseup。detail 的值从 1 开始,每次单击会加 1。如果鼠标在 mousedown和 mouseup 之间移动了,则 detail 会重置为 0。

    IE 还为每个鼠标事件提供了以下额外信息:

    •  altLeft,布尔值,表示是否按下了左 Alt 键(如果 altLeft 是 true,那么 altKey 也是 true);
    •  ctrlLeft,布尔值,表示是否按下了左 Ctrl 键(如果 ctrlLeft 是 true,那么 ctrlKey 也是true);
    •  offsetX,光标相对于目标元素边界的 x 坐标;
    •  offsetY,光标相对于目标元素边界的 y 坐标;
    •  shiftLeft,布尔值,表示是否按下了左 Shift 键(如果 shiftLeft 是 true,那么 shiftKey也是 true)。

    这些属性的作用有限,这是因为只有 IE 支持。而且,它们提供的信息要么没必要,要么可以通过其他方式计算。

    mousewheel 事件

    IE6 首先实现了 mousewheel 事件。之后,Opera、Chrome 和 Safari 也跟着实现了。mousewheel事件会在用户使用鼠标滚轮时触发,包括在垂直方向上任意滚动。这个事件会在任何元素上触发,并(在IE8 中)冒泡到 document 和(在所有现代浏览器中)window。mousewheel 事件的 event 对象包含鼠标事件的所有标准信息,此外还有一个名为 wheelDelta 的新属性。当鼠标滚轮向前滚动时,wheelDelta 每次都是+120;而当鼠标滚轮向后滚动时,wheelDelta 每次都是–120

    可以为页面上的任何元素或文档添加 onmousewheel 事件处理程序,以处理所有鼠标滚轮交互,比如:

    document.addEventListener("mousewheel", (event) => { 
     console.log(event.wheelDelta); 
    });
    

    这个例子简单地显示了鼠标滚轮事件触发时 wheelDelta 的值。多数情况下只需知道滚轮滚动的方向,而这通过 wheelDelta 值的符号就可以知道。

    注意 HTML5 也增加了 mousewheel 事件,以反映大多数浏览器对它的支持。

    触摸屏设备

    iOS 和 Android 等触摸屏设备的实现大相径庭,因为触摸屏通常不支持鼠标操作。在为触摸屏设备开发时,要记住以下事项。

    •  不支持 dblclick 事件。双击浏览器窗口可以放大,但没有办法覆盖这个行为。
    •  单指点触屏幕上的可点击元素会触发 mousemove 事件。如果操作会导致内容变化,则不会再触发其他事件。如果屏幕上没有变化,则会相继触发 mousedown、mouseup 和 click 事件。点触不可点击的元素不会触发事件。可点击元素是指点击时有默认动作的元素(如链接)或指定了 onclick 事件处理程序的元素。
    •  mousemove 事件也会触发 mouseover 和 mouseout 事件。
    •  双指点触屏幕并滑动导致页面滚动时会触发 mousewheel 和 scroll 事件。

    无障碍问题

    如果 Web 应用或网站必须考虑残障人士,特别是使用屏幕阅读器的用户,那么必须小心使用鼠标事件。如前所述,按回车键可以触发 click 事件,但其他鼠标事件不能通过键盘触发。因此,建议不要使用 click 事件之外的其他鼠标事件向用户提示功能或触发代码执行,这是因为其他鼠标事件会严格妨碍盲人或视障用户使用。

    键盘与输入事件

    键盘事件是用户操作键盘时触发的。DOM2 Events 最初定义了键盘事件,但该规范在最终发布前删除了相应内容。因此,键盘事件很大程度上是基于原始的 DOM0 实现的。

    DOM3 Events 为键盘事件提供了一个首先在 IE9 中完全实现的规范。其他浏览器也开始实现该规范,但仍然存在很多遗留的实现。

    键盘事件包含 3 个事件:

    •  keydown,用户按下键盘上某个键时触发,而且持续按住会重复触发。
    •  keypress,用户按下键盘上某个键并产生字符时触发,而且持续按住会重复触发。Esc 键也会触发这个事件。DOM3 Events 废弃了 keypress 事件,而推荐 textInput 事件。
    •  keyup,用户释放键盘上某个键时触发。

    虽然所有元素都支持这些事件,但当用户在文本框中输入内容时最容易看到。

    输入事件只有一个,即 textInput。这个事件是对 keypress 事件的扩展,用于在文本显示给用户之前更方便地截获文本输入。textInput 会在文本被插入到文本框之前触发。

    当用户按下键盘上的某个字符键时,首先会触发 keydown 事件,然后触发 keypress 事件,最后触发 keyup 事件。注意,这里 keydown 和 keypress 事件会在文本框出现变化之前触发,而 keyup事件会在文本框出现变化之后触发。如果一个字符键被按住不放,keydown 和 keypress 就会重复触发,直到这个键被释放。

    对于非字符键,在键盘上按一下这个键,会先触发 keydown 事件,然后触发 keyup 事件。如果按住某个非字符键不放,则会重复触发 keydown 事件,直到这个键被释放,此时会触发 keyup 事件。

    键码

    对于 keydown 和 keyup 事件,event 对象的 keyCode 属性中会保存一个键码,对应键盘上特定的一个键。对于字母和数字键,keyCode 的值与小写字母和数字的 ASCII 编码一致。比如数字 7 键的keyCode 为 55,而字母 A 键的 keyCode 为 65,而且跟是否按了 Shift 键无关。DOM 和 IE 的 event 对象都支持 keyCode 属性。下面这个例子展示了如何使用 keyCode 属性:

    let textbox = document.getElementById("myText"); 
    textbox.addEventListener("keyup", (event) => { 
     console.log(event.keyCode); 
    });
    

    这个例子在 keyup 事件触发时直接显示出 event 对象的 keyCode 属性值。书中给出了键盘上所有非字符键的键码(P519页)。

    字符编码

    keypress 事件发生时,意味着按键会影响屏幕上显示的文本。对插入或移除字符的键,所有浏览器都会触发 keypress 事件,其他键则取决于浏览器。因为 DOM3 Events 规范才刚刚开始实现,所以不同浏览器之间的实现存在显著差异。

    浏览器在 event 对象上支持 charCode 属性,只有发生 keypress 事件时这个属性才会被设置值,包含的是按键字符对应的 ASCII 编码。通常,charCode 属性的值是 0,在 keypress 事件发生时则是对应按键的键码。IE8 及更早版本和 Opera 使用 keyCode 传达字符的 ASCII 编码。要以跨浏览器方式获取字符编码,首先要检查 charCode 属性是否有值,如果没有再使用 keyCode,如下所示:

    var EventUtil = { 
         // 其他代码
         getCharCode: function(event) { 
             if (typeof event.charCode == "number") { 
                return event.charCode; 
             } else { 
                return event.keyCode; 
             } 
         }, 
         // 其他代码
    };
    

    这个方法检测 charCode 属性是否为数值(在不支持的浏览器中是 undefined)。如果是数值,则返回。否则,返回 keyCode 值。可以像下面这样使用:

    let textbox = document.getElementById("myText"); 
    textbox.addEventListener("keypress", (event) => { 
     console.log(EventUtil.getCharCode(event)); 
    });
    

    一旦有了字母编码,就可以使用 String.fromCharCode()方法将其转换为实际的字符了。

    textInput 事件

    DOM3 Events 规范增加了一个名为 textInput 的事件,其在字符被输入到可编辑区域时触发。作为对 keypress 的替代,textInput 事件的行为有些不一样。一个区别是 keypress 会在任何可以获得焦点的元素上触发,而 textInput 只在可编辑区域上触发。另一个区别是 textInput 只在有新字符被插入时才会触发,而 keypress 对任何可能影响文本的键都会触发(包括退格键)。

    因为 textInput 事件主要关注字符,所以在 event 对象上提供了一个 data 属性,包含要插入的字符(不是字符编码)。data 的值始终是要被插入的字符,因此如果在按 S 键时没有按 Shift 键,data的值就是"s",但在按 S 键时同时按 Shift 键,data 的值则是"S"。

    textInput 事件可以这样来用:

    let textbox = document.getElementById("myText"); 
    textbox.addEventListener("textInput", (event) => { 
     console.log(event.data); 
    });
    

    这个例子会实时把输入文本框的文本通过日志打印出来。

    event 对象上还有一个名为 inputMethod 的属性,该属性表示向控件中输入文本的手段。可能的值如下:

    •  0,表示浏览器不能确定是什么输入手段;
    •  1,表示键盘;
    •  2,表示粘贴;
    •  3,表示拖放操作;
    •  4,表示 IME;
    •  5,表示表单选项;
    •  6,表示手写(如使用手写笔);
    •  7,表示语音;
    •  8,表示组合方式;
    •  9,表示脚本

    使用这些属性,可以确定用户是如何将文本输入到控件中的,从而可以辅助验证。

    设备上的键盘事件

    任天堂 Wii 会在用户按下 Wii 遥控器上的键时触发键盘事件。虽然不能访问 Wii 遥控器上所有的键,但其中一些键可以触发键盘事件。

    合成事件

    合成事件是 DOM3 Events 中新增的,用于处理通常使用 IME 输入时的复杂输入序列。IME 可以让用户输入物理键盘上没有的字符。例如,使用拉丁字母键盘的用户还可以使用 IME 输入日文。IME 通常需要同时按下多个键才能输入一个字符。合成事件用于检测和控制这种输入。合成事件有以下 3 种:

    •  compositionstart,在 IME 的文本合成系统打开时触发,表示输入即将开始;
    •  compositionupdate,在新字符插入输入字段时触发;
    •  compositionend,在 IME 的文本合成系统关闭时触发,表示恢复正常键盘输入。

    合成事件在很多方面与输入事件很类似。在合成事件触发时,事件目标是接收文本的输入字段。唯一增加的事件属性是 data,其中包含的值视情况而异:

    •  在 compositionstart 事件中,包含正在编辑的文本(例如,已经选择了文本但还没替换);
    •  在 compositionupdate 事件中,包含要插入的新字符;
    •  在 compositionend 事件中,包含本次合成过程中输入的全部内容。

    与文本事件类似,合成事件可以用来在必要时过滤输入内容。可以像下面这样使用合成事件:

    let textbox = document.getElementById("myText"); 
    textbox.addEventListener("compositionstart", (event) => { 
     console.log(event.data); 
    }); 
    textbox.addEventListener("compositionupdate", (event) => { 
     console.log(event.data); 
    }); 
    textbox.addEventListener("compositionend", (event) => { 
     console.log(event.data); 
    });
    

    HTML5 事件

    contextmenu 事件

    作用:允许开发者取消默认的上下文菜单并提供自定义菜单

    contextmenu 事件冒泡,因此只要给 document 指定一个事件处理程序就可以处理页面上的所有同类事件。事件目标是触发操作的元素。这个事件在所有浏览器中都可以取消,在 DOM 合规的浏览器中使用 event.preventDefault(),在 IE8 及更早版本中将 event.returnValue 设置为 false。contextmenu 事件应该算一种鼠标事件,因此 event 对象上的很多属性都与光标位置有关。通常,自定义的上下文菜单都是通过 oncontextmenu 事件处理程序触发显示,并通过 onclick 事件处理程序触发隐藏的。来看下面的例子:

    <!DOCTYPE html>
    <html>
    
    <head>
      <title>ContextMenu Event Example</title>
    </head>
    
    <body>
      <div id="myDiv">Right click or Ctrl+click me to get a custom context menu.
        Click anywhere else to get the default context menu.</div>
      <ul id="myMenu" style="position:absolute;visibility:hidden;background-color: 
        silver">
        <li><a href="http://www.somewhere.com"> somewhere</a></li>
        <li><a href="http://www.wrox.com">Wrox site</a></li>
        <li><a href="http://www.somewhere-else.com">somewhere-else</a></li>
      </ul>
      <script>
        window.addEventListener("load", (event) => {
          let div = document.getElementById("myDiv");
          div.addEventListener("contextmenu", (event) => {
            event.preventDefault();
            let menu = document.getElementById("myMenu");
            menu.style.left = event.clientX + "px";
            menu.style.top = event.clientY + "px";
            menu.style.visibility = "visible";
          });
          document.addEventListener("click", (event) => {
            document.getElementById("myMenu").style.visibility = "hidden";
          });
        });
      </script>
    </body>
    
    </html>
    

    这个例子中的

    元素有一个上下文菜单
      。作为上下文菜单,
        元素初始时是隐藏的。

        这里在

        元素上指定了一个 oncontextmenu 事件处理程序。这个事件处理程序首先取消默认行,确保不会显示浏览器默认的上下文菜单。接着基于 event 对象的 clientX 和 clientY 属性把
          元素放到适当位置。最后一步通过将 visibility 属性设置为"visible"让自定义上下文菜单显示出来。另外,又给 document 添加了一个 onclick 事件处理程序,以便在单击事件发生时隐藏上下文菜单(系统上下文菜单就是这样隐藏的)。

          虽然这个例子很简单,但它是网页中所有自定义上下文菜单的基础。在这个简单例子的基础上,再添加一些 CSS,上下文菜单就会更漂亮。

          beforeunload 事件

          beforeunload 事件会在 window 上触发,用意是给开发者提供阻止页面被卸载的机会。这个事件会在页面即将从浏览器中卸载时触发,如果页面需要继续使用,则可以不被卸载。这个事件不能取消,否则就意味着可以把用户永久阻拦在一个页面上。相反,这个事件会向用户显示一个确认框,其中的消息表明浏览器即将卸载页面,并请用户确认是希望关闭页面,还是继续留在页面上(见图 17-8)。

          需要将 event.returnValue 设置为要在确认框中显示的字符串(对于 IE 和 Firefox 来说),并将其作为函数值返回(对于 Safari 和 Chrome 来说),如下所示:

          window.addEventListener("beforeunload", (event) => { 
           let message = "I'm really going to miss you if you go."; 
           event.returnValue = message; 
           return message; 
          });
          

          DOMContentLoaded 事件

          window 的 load 事件会在页面完全加载后触发,因为要等待很多外部资源加载完成,所以会花费较长时间。而 DOMContentLoaded 事件会在 DOM 树构建完成后立即触发,而不用等待图片、JavaScript文件、CSS 文件或其他资源加载完成。相对于 load 事件,DOMContentLoaded 可以让开发者在外部资源下载的同时就能指定事件处理程序,从而让用户能够更快地与页面交互。

          要处理 DOMContentLoaded 事件,需要给 document 或 window 添加事件处理程序(实际的事件目标是 document,但会冒泡到 window)。下面是一个在 document 上监听 DOMContentLoaded 事件的例子:

          document.addEventListener("DOMContentLoaded", (event) => { 
           console.log("Content loaded"); 
          });
          

          DOMContentLoaded 事件的 event 对象中不包含任何额外信息(除了 target 等于 document)。

          DOMContentLoaded 事件通常用于添加事件处理程序或执行其他 DOM操作。这个事件始终在 load事件之前触发。

          对于不支持 DOMContentLoaded 事件的浏览器,可以使用超时为 0 的 setTimeout()函数,通过其回调来设置事件处理程序,比如:

          setTimeout(() => { 
           // 在这里添加事件处理程序
          }, 0);
          

          以上代码本质上意味着在当前 JavaScript 进程执行完毕后立即执行这个回调。页面加载和构建期间,只有一个 JavaScript 进程运行。所以可以在这个进程空闲后立即执行回调,至于是否与同一个浏览器或同一页面上不同脚本的 DOMContentLoaded 触发时机一致并无绝对把握。为了尽可能早一些执行,以上代码最好是页面上的第一个超时代码。即使如此,考虑到各种影响因素,也不一定保证能在 load 事件之前执行超时回调。

          readystatechange 事件

          IE 首先在 DOM 文档的一些地方定义了一个名为 readystatechange 事件。这个有点神秘的事件旨在提供文档或元素加载状态的信息,但行为有时候并不稳定。支持 readystatechange 事件的每个对象都有一个 readyState 属性,该属性具有一个以下列出的可能的字符串值。

          •  uninitialized:对象存在并尚未初始化。
          •  loading:对象正在加载数据。
          •  loaded:对象已经加载完数据。
          •  interactive:对象可以交互,但尚未加载完成。
          •  complete:对象加载完成。

          看起来很简单,其实并非所有对象都会经历所有 readystate 阶段。文档中说有些对象会完全跳过某个阶段,但并未说明哪些阶段适用于哪些对象。这意味着 readystatechange 事件经常会触发不到4 次,而 readyState 未必会依次呈现上述值。

          在 document 上使用时,值为"interactive"的 readyState 首先会触发 readystatechange事件,时机类似于 DOMContentLoaded。进入交互阶段,意味着 DOM 树已加载完成,因而可以安全地交互了。此时图片和其他外部资源不一定都加载完了。可以像下面这样使用 readystatechange 事件:

          document.addEventListener("readystatechange", (event) => { 
               if (document.readyState == "interactive") { 
               	console.log("Content loaded"); 
               } 
          });
          

          这个事件的 event 对象中没有任何额外的信息,连事件目标都不会设置

          让问题变得更加复杂的是,交互阶段与完成阶段的顺序也不是固定的。在外部资源较多的页面中,很可能交互阶段会早于完成阶段,而在外部资源较少的页面中,很可能完成阶段会早于交互阶段。因此,实践中为了抢到较早的时机,需要同时检测交互阶段和完成阶段。比如:

          document.addEventListener("readystatechange", (event) => { 
               if (document.readyState == "interactive" || 
               	document.readyState == "complete") { 
                   document.removeEventListener("readystatechange", arguments.callee); 
                   console.log("Content loaded"); 
               } 
          });
          

          当 readystatechange 事件触发时,这段代码会检测 document.readyState 属性,以确定当前是不是交互或完成状态。如果是,则移除事件处理程序,以保证其他阶段不再执行。注意,因为这里的事件处理程序是匿名函数,所以使用了 arguments.callee 作为函数指针。然后,又打印出一条表示内容已加载的消息。这样的逻辑可以保证尽可能接近使用 DOMContentLoaded 事件的效果。

          pageshow 与 pagehide 事件

          Firefox 和 Opera 开发了一个名为往返缓存(bfcache,back-forward cache)的功能,此功能旨在使用浏览器“前进”和“后退”按钮时加快页面之间的切换。这个缓存不仅存储页面数据,也存储 DOM 和JavaScript 状态,实际上是把整个页面都保存在内存里。如果页面在缓存中,那么导航到这个页面时就不会触发 load 事件。通常,这不会导致什么问题,因为整个页面状态都被保存起来了。不过,Firefx决定提供一些事件,把往返缓存的行为暴露出来。

          第一个事件是 pageshow,其会在页面显示时触发,无论是否来自往返缓存。在新加载的页面上,pageshow 会在 load 事件之后触发;在来自往返缓存的页面上,pageshow 会在页面状态完全恢复后触发。注意,虽然这个事件的目标是 document,但事件处理程序必须添加到 window 上。下面的例子展示了追踪这些事件的代码:

          (function() { 
               let showCount = 0; 
               window.addEventListener("load", () => { 
               	console.log("Load fired"); 
               }); 
               window.addEventListener("pageshow", () => { 
              	 showCount++; 
               	console.log(`Show has been fired ${showCount} times.`); 
               }); 
          })();
          

          这个例子使用了私有作用域来保证 showCount 变量不进入全局作用域。在页面首次加载时,showCount 的值为 0。之后每次触发 pageshow 事件,showCount 都会加 1 并输出消息。如果从包含以上代码的页面跳走,然后又点击“后退”按钮返回以恢复它,就能够每次都看到 showCount 递增的值。这是因为变量的状态连同整个页面状态都保存在了内存中,导航回来后可以恢复。如果是点击了浏览器的“刷新”按钮,则 showCount 的值会重置为 0,因为页面会重新加载。

          除了常用的属性,pageshow 的 event 对象中还包含一个名为 persisted 的属性。这个属性是一个布尔值,如果页面存储在了往返缓存中就是 true,否则就是 false。可以像下面这样在事件处理程序中检测这个属性:

          (function() { 
               let showCount = 0; 
               window.addEventListener("load", () => { 
               	console.log("Load fired"); 
               }); 
               window.addEventListener("pageshow", () => { 
               	showCount++; 
               	console.log(`Show has been fired ${showCount} times.`, 
              	 `Persisted? ${event.persisted}`); 
               }); 
          })();
          

          与 pageshow 对应的事件是 pagehide,这个事件会在页面从浏览器中卸载后,在 unload 事件之前触发。与 pageshow 事件一样,pagehide 事件同样是在 document 上触发,但事件处理程序必须被添加到 window。event 对象中同样包含 persisted 属性,但用法稍有不同。比如,以下代码检测了event.persisted 属性:

          window.addEventListener("pagehide", (event) => { 
           console.log("Hiding. Persisted? " + event.persisted); 
          });
          

          这样,当 pagehide 事件触发时,也许可以根据 persisted 属性的值来采取一些不同的操作。对pageshow 事件来说,persisted 为 true 表示页面是从往返缓存中加载的;而对 pagehide 事件来说,persisted 为 true 表示页面在卸载之后会被保存在往返缓存中。因此,第一次触发 pageshow 事件时 persisted 始终是 false,而第一次触发 pagehide 事件时 persisted 始终是 true(除非页面不符合使用往返缓存的条件)。

          hashchange 事件

          HTML5 增加了 hashchange 事件,用于在 URL 散列值(URL 最后#后面的部分)发生变化时通知开发者。这是因为开发者经常在 Ajax 应用程序中使用 URL 散列值存储状态信息或路由导航信息。

          onhashchange 事件处理程序必须添加给 window,每次 URL 散列值发生变化时会调用它。event对象有两个新属性:oldURL 和 newURL。这两个属性分别保存变化前后的 URL,而且是包含散列值的完整 URL。下面的例子展示了如何获取变化前后的 URL:

          window.addEventListener("hashchange", (event) => { 
           console.log(`Old URL: ${event.oldURL}, New URL: ${event.newURL}`); 
          });
          

          如果想确定当前的散列值,最好使用 location 对象:

          window.addEventListener("hashchange", (event) => { 
           console.log(`Current hash: ${location.hash}`); 
          });
          

          设备事件

          orientationchange 事件

          苹果公司在移动 Safari 浏览器上创造了 orientationchange 事件,以方便开发者判断用户的设备是处于垂直模式还是水平模式。移动 Safari 在 window 上暴露了 window.orientation 属性,它有以下 3 种值之一:0 表示垂直模式,90 表示左转水平模式(主屏幕键在右侧),–90 表示右转水平模式(主屏幕键在左)。虽然相关文档也提及设备倒置后的值为 180,但设备本身至今还不支持。

          每当用户旋转设备改变了模式,就会触发 orientationchange 事件。但 event 对象上没有暴露任何有用的信息,这是因为相关信息都可以从 window.orientation 属性中获取。以下是这个事件典型的用法:

          window.addEventListener("load", (event) => { 
               let div = document.getElementById("myDiv"); 
               div.innerHTML = "Current orientation is " + window.orientation; 
               window.addEventListener("orientationchange", (event) => { 
               	div.innerHTML = "Current orientation is " + window.orientation; 
               }); 
          });
          

          这个例子会在 load 事件触发时显示设备初始的朝向。然后,又指定了 orientationchange 事件处理程序。此后,只要这个事件触发,页面就会更新以显示新的朝向信息。

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

          注意 因为 orientationchange 事件被认为是 window 事件,所以也可以通过给元素添加 onorientationchange 属性来指定事件处理程序。

          deviceorientation 事件

          deviceorientation 是DeviceOrientationEvent规范定义的事件。如果可以获取设备的加速计信息,而且数据发生了变化,这个事件就会在 window 上触发。要注意的是,deviceorientation 事件只反映设备在空间中的朝向,而不涉及移动相关的信息。

          设备本身处于 3D 空间即拥有 x 轴、y 轴和 z 轴的坐标系中。如果把设备静止放在水平的表面上,那么三轴的值均为 0,其中,x 轴方向为从设备左侧到右侧,y 轴方向为从设备底部到上部,z 轴方向为从设备背面到正面,

          当 deviceorientation 触发时,event 对象中会包含各个轴相对于设备静置时坐标值的变化,主要是以下 5 个属性。

          •  alpha:0~360 范围内的浮点值,表示围绕 z 轴旋转时 y 轴的度数(左右转)。
          •  beta:–180~180 范围内的浮点值,表示围绕 x 轴旋转时 z 轴的度数(前后转)。
          •  gamma:–90~90 范围内的浮点值,表示围绕 y 轴旋转时 z 轴的度数(扭转)。
          •  absolute:布尔值,表示设备是否返回绝对值。
          •  compassCalibrated:布尔值,表示设备的指南针是否正确校准。

          下面是一个输出 alpha、beta 和 gamma 值的简单例子:

          window.addEventListener("deviceorientation", (event) => { 
           let output = document.getElementById("output"); 
           output.innerHTML = 
           `Alpha=${event.alpha}, Beta=${event.beta}, Gamma=${event.gamma}<br>`; 
          });
          

          基于这些信息,可以随着设备朝向的变化重新组织或修改屏幕上显示的元素。例如,以下代码会随着朝向变化旋转一个元素:

          window.addEventListener("deviceorientation", (event) => { 
           let arrow = document.getElementById("arrow"); 
           arrow.style.webkitTransform = `rotate(${Math.round(event.alpha)}deg)`; 
          });
          

          这个例子只适用于移动 WebKit 浏览器,因为使用的是专有的 webkitTransform 属性(CSS 标准的 transform 属性的临时版本)。“箭头”(arrow)元素会随着 event.alpha 值的变化而变化,呈现出指南针的样子。这里给 CSS3 旋转变形函数传入了四舍五入后的值,以确保平顺。

          devicemotion 事件

          DeviceOrientationEvent 规范也定义了 devicemotion 事件。这个事件用于提示设备实际上在移动,而不仅仅是改变了朝向。例如,devicemotion 事件可以用来确定设备正在掉落或者正拿在一个行走的人手里。

          当 devicemotion 事件触发时,event 对象中包含如下额外的属性。

          •  acceleration:对象,包含 x、y 和 z 属性,反映不考虑重力情况下各个维度的加速信息。
          •  accelerationIncludingGravity:对象,包含 x、y 和 z 属性,反映各个维度的加速信息,包含 z 轴自然重力加速度。
          •  interval:毫秒,距离下次触发 devicemotion 事件的时间。此值在事件之间应为常量。
          •  rotationRate:对象,包含 alpha、beta 和 gamma 属性,表示设备朝向。

          如果无法提供 acceleration、accelerationIncludingGravity 和 rotationRate 信息,则属性值为 null。为此,在使用这些属性前必须先检测它们的值是否为 null。比如:

          window.addEventListener("devicemotion", (event) => {
              let output = document.getElementById("output");
              if (event.rotationRate !== null) {
                  output.innerHTML += `Alpha=${event.rotationRate.alpha}` +
                      `Beta=${event.rotationRate.beta}` +
                      `Gamma=${event.rotationRate.gamma}`;
              }
          });
          

          触摸及手势事件

          触摸事件

          当手指放在屏幕上、在屏幕上滑动或从屏幕移开时,触摸事件即会触发。触摸事件有如下几种。

          •  touchstart:手指放到屏幕上时触发(即使有一个手指已经放在了屏幕上)。
          •  touchmove:手指在屏幕上滑动时连续触发。在这个事件中调用 preventDefault()可以阻止滚动。
          •  touchend:手指从屏幕上移开时触发。
          •  touchcancel:系统停止跟踪触摸时触发。文档中并未明确什么情况下停止跟踪。

          这些事件都会冒泡,也都可以被取消。尽管触摸事件不属于 DOM 规范,但浏览器仍然以兼容 DOM的方式实现了它们。因此,每个触摸事件的 event 对象都提供了鼠标事件的公共属性:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和 metaKey。

          除了这些公共的 DOM 属性,触摸事件还提供了以下 3 个属性用于跟踪触点

          •  touches:Touch 对象的数组,表示当前屏幕上的每个触点。
          •  targetTouches:Touch 对象的数组,表示特定于事件目标的触点。
          •  changedTouches:Touch 对象的数组,表示自上次用户动作之后变化的触点。

          每个 Touch 对象都包含下列属性。

          •  clientX:触点在视口中的 x 坐标。
          •  clientY:触点在视口中的 y 坐标。
          •  identifier:触点 ID。
          •  pageX:触点在页面上的 x 坐标。
          •  pageY:触点在页面上的 y 坐标。
          •  screenX:触点在屏幕上的 x 坐标。
          •  screenY:触点在屏幕上的 y 坐标。
          •  target:触摸事件的事件目标。

          这些属性可用于追踪屏幕上的触摸轨迹。例如:

          function handleTouchEvent(event) {
              // 只针对一个触点
              if (event.touches.length == 1) {
                  let output = document.getElementById("output");
                  switch (event.type) {
                      case "touchstart":
                          output.innerHTML += `<br>Touch started:` +
                              `(${event.touches[0].clientX}` +
                              ` ${event.touches[0].clientY})`;
                          break;
                      case "touchend":
                          output.innerHTML += `<br>Touch ended:` +
                              `(${event.changedTouches[0].clientX}` +
                              ` ${event.changedTouches[0].clientY})`;
                          break;
                      case "touchmove":
                          event.preventDefault(); // 阻止滚动
                          output.innerHTML += `<br>Touch moved:` +
                              `(${event.changedTouches[0].clientX}` +
                              ` ${event.changedTouches[0].clientY})`;
                          break;
                  }
              }
          }
          document.addEventListener("touchstart", handleTouchEvent);
          document.addEventListener("touchend", handleTouchEvent);
          document.addEventListener("touchmove", handleTouchEvent);
          

          以上代码会追踪屏幕上的一个触点。为简单起见,代码只会在屏幕有一个触点时输出信息。在touchstart 事件触发时,触点的位置信息会输出到 output 元素中。在 touchmove 事件触发时,会取消默认行为以阻止滚动(移动触点通常会滚动页面),并输出变化的触点信息。在 touchend 事件触发时,会输出触点最后的信息。注意,touchend 事件触发时 touches 集合中什么也没有,这是因为没有滚动的触点了。此时必须使用 changedTouches 集合。

          这些事件会在文档的所有元素上触发,因此可以分别控制页面的不同部分。当手指点触屏幕上的元素时,依次会发生如下事件(包括鼠标事件):

          • (1) touchstart
          • (2) mouseover
          • (3) mousemove(1 次)
          • (4) mousedown
          • (5) mouseup
          • (6) click
          • (7) touchend

          手势事件

          手势事件会在两个手指触碰屏幕且相对距离或旋转角度变化时触发。手势事件有以下 3 种。

          •  gesturestart:一个手指已经放在屏幕上,再把另一个手指放到屏幕上时触发。
          •  gesturechange:任何一个手指在屏幕上的位置发生变化时触发。
          •  gestureend:其中一个手指离开屏幕时触发。

          只有在两个手指同时接触事件接收者时,这些事件才会触发。在一个元素上设置事件处理程序,意味着两个手指必须都在元素边界以内才能触发手势事件(这个元素就是事件目标)。因为这些事件会冒泡,所以也可以把事件处理程序放到文档级别,从而可以处理所有手势事件。使用这种方式时,事件的目标就是两个手指均位于其边界内的元素。

          触摸事件和手势事件存在一定的关系。当一个手指放在屏幕上时,会触发 touchstart 事件。当另一个手指放到屏幕上时,gesturestart 事件会首先触发,然后紧接着触发这个手指的 touchstart事件。如果两个手指或其中一个手指移动,则会触发 gesturechange 事件。只要其中一个手指离开屏幕,就会触发 gestureend 事件,紧接着触发该手指的 touchend 事件。

          与触摸事件类似,每个手势事件的 event 对象都包含所有标准的鼠标事件属性:bubbles、cancelable、view、clientX、clientY、screenX、screenY、detail、altKey、shiftKey、ctrlKey 和 metaKey。新增的两个 event 对象属性是 rotation 和 scale。rotation 属性表示手指变化旋转的度数,负值表示逆时针旋转,正值表示顺时针旋转(从 0 开始)。scale 属性表示两指之间距离变化(对捏)的程度。开始时为 1,然后随着距离增大或缩小相应地增大或缩小。

          可以像下面这样使用手势事件的属性:

          function handleGestureEvent(event) {
              let output = document.getElementById("output");
              switch (event.type) {
                  case "gesturestart":
                      output.innerHTML += `Gesture started: ` +
                          `rotation=${event.rotation},` +
                          `scale=${event.scale}`;
                      break;
                  case "gestureend":
                      output.innerHTML += `Gesture ended: ` +
                          `rotation=${event.rotation},` +
                          `scale=${event.scale}`;
                      break;
                  case "gesturechange":
                      output.innerHTML += `Gesture changed: ` +
                          `rotation=${event.rotation},` +
                          `scale=${event.scale}`;
                      break;
              }
          }
          document.addEventListener("gesturestart", handleGestureEvent, false);
          document.addEventListener("gestureend", handleGestureEvent, false);
          document.addEventListener("gesturechange", handleGestureEvent, false);
          

          与触摸事件的例子一样,以上代码简单地将每个事件对应到一个处理函数,然后输出每个事件的信息

          注意 触摸事件也会返回 rotation 和 scale 属性,但只在两个手指触碰屏幕时才会变化。一般来说,使用两个手指的手势事件比考虑所有交互的触摸事件使用起来更容易一些。