深入理解现代浏览器 4: 浏览器视角看输入事件
目录
译注:本文翻译自 ChromeDeveloper 博客, 原文发表于2018年底, 部分特性可能与目前 Chrome 有所不同。建议有条件的读者直接阅读原文。
本章是深入理解现代浏览器系列四章中的第后一章, 研究如何处理我们的代码并显示网页。之前的几章中, 我们研究了渲染进程,并了解了合成器。本章我们将研究当用户输入时,合成器如何实现与用户的流畅交互。
当说到"输入事件(input events)" , 你可能只会想到在文本框输入字符或点击鼠标,但以浏览器的视角来看,输入意味着用户的任何手势(gesture)。鼠标滚动是,触摸(touch)是, 鼠标悬停也是。
当用户触摸屏幕时,浏览器进程是首先接收到该手势的进程。但是, 因为标签内的内容是由渲染器进程处理的,browser 进程只知道该手势的位置。所以浏览器进程将事件类型(如 touchstart
) 和其坐标发送给渲染器进程。渲染器进程通过查找事件目标及运行其附加的事件监听器来适当地处理处理事件。
合成器接收输入事件
在之前几章我们研究了合成器是如何通过合成栅格化的图层来平滑地处理滚动(scroll)的。如果页面没有绑定输入事件监听器,合成器线程可以创建一个完全独立于主线程的合成帧。但是, 如果页面绑定了事件监听器呢?合成器线程是如何知道事件是否需要被处理呢?
理解非快速滚动区域
由于运行 JS 是主线程的工作,当页面完成合成,合成器将有绑定事件处理器的页面区域标记为非快速滚动区域(Non-Fast Scrollable Region). 通过获取此信息,合成器线程可以在发生在此区域的输入事件发送到主线程。如果输入事件来自此区域之外,那么合成器线程将继续合成新帧而无需等待主线程。
编写事件处理器时的注意事项
在 web 开发中常见的事件处理模式是事件委托(event delegation)。由于冒泡(event bubble), 你可以在最上层的元素上绑定一个事件处理器,并基于事件目标委托事件。你可能见过或写过如下代码:
1 2 3 4 5 |
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault(); } }); |
由于你只需要为所有元素编写一个事件处理器,这种事件委托的模式很具吸引力。但是如果你从浏览器的视角来看这段代码,现在整个页面都被标记为非快速滚动区域, 这意味着,每当输入事件发生时,即使你的程序并不关心页面某个部分的输入,而合成器也必须得和主线程通信并等待之。于此,合成器的平滑滚动能力被干败.
为了减轻这种情况, 你可以在事件监听器中传递 passive:true
选项。这向浏览器表示, 你仍想在主线程中监听事件,但合成器也可以继续合成新帧。
1 2 3 4 5 |
document.body.addEventListener('touchstart', event => { if (event.target === area) { event.preventDefault() } }, {passive: true}); |
检查事件是否可被取消
假设你在页面有一个框,你希望将滚动方向限制为仅水平滚动。在指针事件中使用 passive: true
选项意味着页面可以平滑滚动。但垂直滚动可能在你想 preventDefault
时已经开始,以限制滚动方向。你可以通过 event.cancelable
方法来检查。
1 2 3 4 5 6 7 8 |
document.body.addEventListener('pointermove', event => { if (event.cancelable) { event.preventDefault(); // block the native scroll /* * do what you want the application to do here */ } }, {passive: true}); |
再或者,你可以使用 CSS 规则 touch-action
来完全地消除事件处理器。
1 2 3 |
#area { touch-action: pan-x; } |
查找事件目标
当合成器线程将输入事件发送到主线程,首先要做的就是运行一个 命中测试(hit test) 来查找事件目标。"hit test" 使用渲染进程生成的绘制记录数据来查找事件发生地坐标下的内容。
最小化对主线程的事件分发
在前几章,我们讨论了我们需要在典型的每秒刷新60次的浏览器中跟上流畅地跟上动画的节奏。对于输入,典型的触屏设备每秒传送60到120次触摸事件,而典型鼠标每秒传送100次事件。输入事件的保真度(fidelity)比我们屏幕刷新率更高。
如果每秒向主线程传输120次如 touchmove
这样的持续事件,那么与屏幕刷新率相比,这可能会触发过多的命中测试与JS 执行。
为了尽量减少对主线程的冗余调用, Chrome 会合并连续事件(如 wheel , mousewheel , mousemove , pointermove , touchmove
) 且延迟分发, 直到下一个 requestAnimationFrame
.
面任何离散事件 keydown, keyup, mouseup, mousedown, touchstart, and touchend
, 则会立即分发。
使用 getCoalescedEvents
来获取内部帧(intra-frame)事件
对于大部分 web 程序, 合并事件足以提供良好的用户体验。然而,如果你正在构建像绘制和基于 touchmove
坐标来放置路径之类的程序, 你可能会失去用来画平滑线条的中间坐标, 你可以使用指针事件中的 getCoalescedEvents
方法来获取合并过的事件。
1 2 3 4 5 6 7 8 |
window.addEventListener('pointermove', event => { const events = event.getCoalescedEvents(); for (let event of events) { const x = event.pageX; const y = event.pageY; // draw a line using x and y coordinates. } }); |
接下来
这几章我们研究了浏览器的内部工作。如果你从来没想过为何开发者工具推荐在你的事件处理器中添加 {passive: true}
, 或为何你会在你的 script tag 里写上 async
属性, 我希望本系列能够阐明为何浏览器需要这些信息来理念快, 理念平滑的web 体验。
使用 Lighthouse
如果你想让你的代码变得对浏览器更好而你不知道如何开始,可以看看 Lighthouse 它是一种可以对任何网站进行审查(audit) , 并提供一份什么做对了、什么需要改进的报告的工具。阅读审查列表也可以了解浏览器关心浏览器关心的是什么。
了解如何衡量性能
不同网站的性能有所不同。衡量你网站的性能并确定最适合你网站的内容很重要。 Chrome 开发者工具团队有些建议: how to measure your site's performance.
添加功能策略
如果你想更进一步, 功能策略 是一项新的web平台功能, 它可以在构建项目时提供防护。启用功能策略可以保证您程序的某些行为防止您犯错。比如, 如果你想确保你的程序的解析不会被阻塞,你可以在同步解析策略上运行你的程序。当 sync-script: 'none'
时,阻塞解析的 JS 将会被阻止执行。这可以防止你的任何代码阻塞解决器,浏览器也无需担心解析器被阻塞。
小节
当我开始构建网站时, 我几乎只关心如何编写代码以及怎样提高工作效率,这很重要, 但是我们也应该考虑浏览器如何处理我们写的代码。现代浏览器已经致力于为用户提供更好的web 体验。组织对浏览器更友好的代码,另一方面可以改善你的用户体验。望你我一起追求对浏览器更友好的代码。
超感谢审阅本系列稿子的各位, 包括但不限于 Alex Russell, Paul Irish, Meggin Kearney, Eric Bidelman, Mathias Bynens, Addy Osmani, Kinuko Yasuda, Nasko Oskov, and Charlie Reis.