Google 图解 Chrome:浏览器也讲究事件分发策略 | 完结篇

2018-10-24 14:27:45 / 打印

Chrome 算是程序员的标配了,从全球的市场份额来看,它在全球市场的份额已经超过 60%。

在 Chrome 10 周年之际,官方发布了一个系列文章,用图解的方式,很清晰的讲解了现代浏览器的运行原理。

本文是该系列的第四篇,为了便于阅读,我做了简单修改和注释,建议顺序阅读:

本系列主要是为了帮助你了解,写的代码在浏览器上到底是如何被解析并执行的。老规矩,觉得本文有帮助,就点赞、留言并转发分享吧,你喜欢总要让我知道吧!

这是关于现代浏览器系列博客的第四篇,也是最后一篇。在上一篇文章中,我们研究了渲染过程并了解了合成器进程。在这篇文章中,我们将看看用户进行输入时,合成器如何实现平滑的交互。

当聊到“输入事件”时,你可能只会想文本框输入或者鼠标点击,但是从浏览器的角度来看,“输入”意味着来自用户的任何操作。鼠标滚轮滚动是输入事件,触摸或鼠标移动也是输入事件。

当发生用户在屏幕上触摸手势时,浏览器进程是首先接收手势的动作过程。但是浏览器进程只知道该手势发生的位置,因为选项卡内部的内容是由渲染器进程处理。因此,浏览器进程将事件类型及其坐标发送到渲染器进程,渲染器进程通过查找事件目标并运行附加的事件监听器来适当的处理事件。

在上一篇文章中,介绍了合成器如何通过合成光栅化的图层来平滑地处理滚动。如果输入事件监听器附加到页面上,则合成器进程可以创建完全独立于主线程的新复合帧。但是如果一些事件监听被附加到页面上呢?如果需要处理这些事件,合成器线程如何区分和处理?

由于 JavaScript 是运行在主线程的,因此当合成页面时,合成器线线程会标记页面的一个区域,该区域将事件处理程序附加到“非快速可滚动区域”。

通过获取此信息,合成器线程可以确保在该区域发生事件时,将输入事件发送到主线程。

如果输入事件来自该区域之外,则合成器线程会在不等待主线程响应的情况下,合成新帧。

在上图中,描述了非快速可滚动区域的事件输入图。

Web 开发中,常见的事件处理模式是事件委派。由于事件冒泡,你可以在最顶层的元素上附加一个事件处理程序,并根据事件的来源委派任务。

你可能已经看过或者曾经编写过类似的代码:

document.body.addEventListener('touchstart', 
event => {    if (event.target === area) {        event.preventDefault();    }});由于你现在可以为所有元素编写一个事件处理监听,因此该事件委托模式的设计就很有吸引力了。但是,如果你从浏览器的角度来看这段代码,此时整个页面都被标记为“非快速可滚动区域”。

这意味着即使你的代码不关心页面某些部分的输入情况,合成器线程也必须与主线程通信,并在每次输入事件的时候等待它的响应。而这将破坏合成器实现平滑滚动的能力。

如上图,描述覆盖整个页面的非快速可滚动区域的输入效果。

为了减少这种情况的发生,可以在事件监听器中,传递 passive:true 参数,这将向浏览器表明,仍然希望在主线程中监听事件,但是合成器也可以继续合成新帧。

document.body.addEventListener('touchstart', 
event => {    if (event.target === area) {        event.preventDefault()    } }, {passive: true});检查事件是否可被取消想象一下,你在网页内有一个框,你希望将它限制为仅支持水平方向滚动。

使用 passive:true 这个属性意味着在页面滚动的时候,可以实现平滑滚动。但是你想要使用 preventDefault 来限制滚动方向时,可能已经开始垂直滚动了。在此之前,你可以使用 event.cancelable 方法对此进行检查。

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 来完全消除触摸事件的影响。

#area {   touch-action: pan-x; }查找事件目标当合成器线程想主线程发送输入事件时,首先要运行命中检测,来查找触发事件的目标源。

命中检测利用渲染过程中,生成的绘制记录数据来查找事件发生的点的坐标下的具体内容。

在之前的文章中我们讨论过,通常显示器每秒刷新屏幕 60 次,以及浏览器如何跟上这个节奏,来获得流畅的动画体验。对于输入,触摸屏设备每秒发生 60~120 次触摸事件,而典型的鼠标(滑动)则每秒发送 100 次事件。输入事件具有比屏幕刷新更高的频率。

如果连续事件 touchmove ,每秒发送事件到主线程 120 次,那么与屏幕刷新速度相比,它可能会触发过多的命中检测和 JavaScript 执行。

如上图所示的情况,如果整个时间线上充斥着事件,会导致页面的抖动。

为了尽量减少对主线程过度的调用,Chrome 将采用合并连续事件(如: wheel、mousewheel、mousemove、pointermove、touchmove) 和 延迟派遣事件直到前面调用 requestAnimationFrame。

如上图所示,在实际的时间线上,时间会被合并并延迟发送。

除了这些事件之外,其他离散的事件,例如:keydown、keyup、mouseup、mousedown、touchstart 和 touchend,将被立即派遣发送。

对于大部分 Web 应用程序,合并事件已经足以提供良好的用户体验。但是,如果要构建绘制相关的应用程序,并根据 touchmove 的坐标,移动的路径进行绘制内容,则可能会丢失中间的坐标而导致绘制成直线。在这种情况下,你可以使用 getCoalescedEvents 指针事件中的方法,来获取有关合并事件的相关信息。

如上图,左侧是平滑的触摸手势路径,而右侧则是合并后的路径。

如果你想让你的代码,对浏览器友好,但是又不知道从何着手,Lighthouse 是一个不错的工具,它可以检查任何网站,并为你提供一个正确的情况报告以及改进意见。阅读检查列表,还可以让你了解更多浏览器相关的内容。

不同网站的性能调整,侧重点可能有所不同。因此衡量网站性能并确定那种策略最合适你的网站,就至关重要。Chrome DevTools 团队有一个完整的教程。

Feature Policy 是一个新的 Web 平台性功能,如果你想执行额外的功能,可以通过它在你构建项目时,提供边界护栏。启动 Feature Policy 可以确保应用程序的某些行为被容错并防止你出错。

例如:如果要确保应用程序永远不会阻止解析,可以在同步脚本策略上运行你的程序代码,当 sysnc-script:'none' 被启动时,JavaScript 解析器将不会被阻断,这可以防止你的任何代码阻断解析器,并且浏览器也不需要担心暂停解析器。

当我们开始构建网站时,我几乎只关心如何编写代码,以及怎样能够帮助我提高工作效率。这些事情很重要,但我们也应该考虑浏览器如何处理我们编写的代码。

现代浏览器以及开始并将持续投入资源,为用户提供更好的 Web 体验。反过来,通过编写对浏览器友好的代码,就可以改善用户体验,这是我们共同努力的目标。

「」