以前,指向网页上的对象很简单。您有鼠标,您移动鼠标,有时按按钮,就这样。所有非鼠标设备都被模拟为鼠标,开发者可以完全放心。
不过,简单并不一定意味着好。随着时间的推移,我们越来越重视多样性:您可以使用具有压感和倾斜感知功能的触控笔,获得极大的创作自由;您也可以使用手指,只需设备和手即可;而且,何不同时使用多根手指?
我们已经推出了触摸事件来帮助我们解决此问题,但它们是专门用于触摸的完全独立的 API,因此如果您想同时支持鼠标和触摸,就必须编写两个单独的事件模型。Chrome 55 附带了一种新标准,可统一这两种模型:指针事件。
单事件模型
指针事件统一了浏览器的指针输入模型,将触摸、触控笔和鼠标整合到一组事件中。例如:
document.addEventListener('pointermove', ev => console.log('The pointer moved.')); foo.addEventListener('pointerover', ev => console.log('The pointer is now over foo.')); 以下是所有可用事件的列表,如果您熟悉鼠标事件,应该会觉得这些事件非常熟悉:
pointerover | 指针已进入元素的边界框。 对于支持悬停的设备,此操作会立即发生;对于不支持悬停的设备,此操作会在 pointerdown 事件之前发生。 |
pointerenter | 与 pointerover 类似,但不会向上传播,并且会以不同的方式处理子项。 规范详情。 |
pointerdown | 指针已进入活动按钮状态,即按下按钮或建立接触,具体取决于输入设备的语义。 |
pointermove | 指针已更改位置。 |
pointerup | 指针已离开活动按钮状态。 |
pointercancel | 发生了某种情况,指针不太可能再发出任何事件。这意味着,您应取消所有正在进行的操作,并返回中性输入状态。 |
pointerout | 指针已离开元素或屏幕的边界框。此外,如果设备不支持悬停,则在 pointerup 后面也需要添加此属性。 |
pointerleave | 与 pointerout 类似,但不会向上传播,并且会以不同的方式处理子项。 规范详情。 |
gotpointercapture | 元素已收到指针捕获。 |
lostpointercapture | 正在捕获的指针已释放。 |
不同的输入类型
通常,借助指针事件,您可以以与输入无关的方式编写代码,而无需为不同的输入设备注册单独的事件处理脚本。当然,您仍然需要注意输入类型之间的差异,例如悬停概念是否适用。不过,如果您确实想区分不同的输入设备类型(例如,为不同的输入提供单独的代码/功能),则可以使用 PointerEvent 接口的 pointerType 属性在同一事件处理脚本中执行此操作。例如,如果您要编写侧边抽屉导航栏,则可以为 pointermove 事件使用以下逻辑:
switch(ev.pointerType) { case 'mouse': // Do nothing. break; case 'touch': // Allow drag gesture. break; case 'pen': // Also allow drag gesture. break; default: // Getting an empty string means the browser doesn't know // what device type it is. Let's assume mouse and do nothing. break; } 默认操作
在支持触控的浏览器中,可以使用某些手势滚动、缩放或刷新网页。 对于触摸事件,您仍会在这些默认操作执行期间收到事件,例如,在用户滚动时,系统仍会触发 touchmove。
使用指针事件时,每当触发滚动或缩放等默认操作时,您都会收到 pointercancel 事件,以便您知道浏览器已控制指针。例如:
document.addEventListener('pointercancel', ev => console.log('Go home, the browser is in charge now.')); 内置速度:与触摸事件相比,此模型默认可提供更好的性能,因为在触摸事件中,您需要使用被动事件监听器才能实现相同的响应速度。
您可以使用 touch-action CSS 属性阻止浏览器接管。将元素上的此属性设置为 none 会停用通过该元素启动的所有浏览器定义的操作。不过,还有一些其他值可用于实现更精细的控制,例如 pan-x,可让浏览器对 x 轴上的移动做出响应,但对 y 轴上的移动不响应。Chrome 55 支持以下值:
auto | 默认;浏览器可以执行任何默认操作。 |
none | 浏览器不得执行任何默认操作。 |
pan-x | 浏览器只能执行水平滚动默认操作。 |
pan-y | 浏览器只能执行纵向滚动默认操作。 |
pan-left | 浏览器只能执行水平滚动默认操作,并且只能将页面平移到左侧。 |
pan-right | 浏览器只能执行水平滚动默认操作,并且只能将页面平移到右侧。 |
pan-up | 浏览器只能执行垂直滚动默认操作,并且只能向上平移页面。 |
pan-down | 浏览器只能执行垂直滚动默认操作,并且只能向下平移页面。 |
manipulation | 浏览器只能执行滚动和缩放操作。 |
指针捕获
您是否曾花费数小时来调试损坏的 mouseup 事件,最后才发现问题出在用户在点击目标之外放开按钮上?没有?好的,可能只是我的问题。
不过,到目前为止,还没有很好的方法来解决这个问题。当然,您可以在文档中设置 mouseup 处理脚本,并在应用中保存一些状态以跟踪各项内容。不过,这并不是最干净的解决方案,尤其是在您构建Web 组件并尝试将所有内容都保持干净隔离的情况下。
指针事件提供了一个更好的解决方案:您可以捕获指针,以确保收到 pointerup 事件(或任何其他难以捉摸的事件)。
const foo = document.querySelector('#foo'); foo.addEventListener('pointerdown', ev => { console.log('Button down, capturing!'); // Every pointer has an ID, which you can read from the event. foo.setPointerCapture(ev.pointerId); }); foo.addEventListener('pointerup', ev => console.log('Button up. Every time!')); 浏览器支持
在撰写本文时,Internet Explorer 11、Microsoft Edge、Chrome 和 Opera 支持指针事件,Firefox 部分支持指针事件。您可以访问 caniuse.com 查看最新列表。
您可以使用指针事件 polyfill 来填补空白。或者,您也可以直接在运行时检查浏览器支持情况:
if (window.PointerEvent) { // Yay, we can use pointer events! } else { // Back to mouse and touch events, I guess. } 指针事件非常适合渐进式增强:只需修改初始化方法以执行上述检查,在 if 块中添加指针事件处理脚本,并将鼠标/触摸事件处理脚本移至 else 块即可。
欢迎试用,并与我们分享您的感受和想法!