现代浏览器探秘(part4):事件处理

478 阅读8分钟

翻译:疯狂的技术宅

原文:developers.google.com/web/updates…

本文首发微信公众号:jingchengyideng 点击下面链接查看其它章节文章


当输入到达合成器

这是关于Chrome浏览器内部工作原理系列的最后一篇;研究浏览器怎样通过处理代码来显示网站。在上一篇文章中,我们研究了渲染过程并了解了合成器。 在本文中,我们将分析当用户输入时,合成器是怎样实现平滑交互的。

从浏览器的角度看输入事件

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

当发生类似在屏幕上的触摸的用户动作时,浏览器是最先先接收到动作的进程之一,但是浏览器进程只知道该动作发生的位置。因为选项卡内部的内容由渲染器进程处理,所以浏览器进程会把事件类型(如touchstart)及其坐标发送到渲染器进程。 渲染器进程通过查找事件目标并运行附加的事件侦听器来适当地处理事件。

图1:通过浏览器进程路由到渲染器进程的输入事件

图1:通过浏览器进程路由到渲染器进程的输入事件

合成器接收输入事件

在上一篇文章中,我们研究了合成器是如何通过合成栅格化图层来平滑地处理滚动的。 如果没有输入事件侦听器附加到页面,那么合成器线程可以创建完全独立于主线程的新复合帧。 但是如果一些事件监听器被附加到页面上会怎样呢? 如果需要处理事件,合成器线程将如何操作呢?

图2:将鼠标悬停在页面图层上

了解非快速可滚动区域

由于JavaScript是运行在主线程上的,所以当合成页面时,合成器线程会标记页面的一个区域,该区域将事件处理程序附加为“非快速可滚动区域”。通过获取此信息,合成器线程可以确保在该区域中发生事件时将输入事件发送到主线程。 如果输入事件来自该区域之外,则合成器线程在不等待主线程的情况下进行合成新帧。

图3:输入到非快速可滚动区域的示意图

图3:输入到非快速可滚动区域的示意图

在编写事件处理程序时要注意

Web开发中常见的事件处理模式是事件委托。 由于事件冒泡,你可以在最顶层的元素上附加一个事件处理程序,并根据事件目标委派任务。 你可能看到过或写过类似下面的代码。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault();
    }
});

由于你只需要为所有元素编写一个事件处理程序,因此该事件委托模式在工程上很有吸引力。 但是如果从浏览器的角度来看这段代码,整个页面都被标记成了非快速可滚动区域。那么这意味着什么呢?即使你的应用不关心页面中某些部分的输入,合成器线程也必须与主线程通信,并且在每次输入事件进入时都要等待它。因此合成器的平滑滚动能力被破坏了。

图4:在覆盖整个页面的非快速可滚动区域进行输入

图4:在覆盖整个页面的非快速可滚动区域进行输入

为了缓解这种情况,你可以在事件侦听器中传递passive:true选项。 这向浏览器提示你仍然希望在主线程中监听事件,同时合成器也可以继续并合成新帧。

document.body.addEventListener('touchstart', event => {
    if (event.target === area) {
        event.preventDefault()
    }
 }, {passive: true});

检查事件是否可取消

想象一下,在页面中有一个框,你希望仅将滚动方向限制为水平滚动。

在鼠标事件中使用 passive:true 选项意味着可以平滑滚动页面,但是在你想要用preventDefault 来限制滚动方向时,垂直滚动可能已经开始了。 你可以使用event.cancelable方法对这种情况进行检查。

图5:一个部分内容被固定为水平滚动的网页

图5:一个部分内容被固定为水平滚动的网页

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;
}

查找事件目标

当合成器线程向主线程发送输入事件时,首先要做的是命中测试以查找事件目标。 命中测试查找事件发生的坐标之下的内容,它使用在渲染进程中生成的绘制记录数据来完成这一使命。

图6:查看绘制记录的主线程询问在x.y坐标点上绘制的内容

图6:查看绘制记录的主线程询问在x.y坐标点上绘制的内容

最小化事件发送到主线程

在上一篇文章中,我们讨论了我们的显示器以每秒60次的频率刷新的机制,以及我们怎样跟上节奏来获得流畅的动画效果。 对于输入来说,典型的触摸屏设备每秒发送60-120次触摸事件,而典型的鼠标每秒发送100次事件。 输入事件具有比屏幕刷新更高的保真度。

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

图7:充斥在帧时间线上的事件导致页面闪烁

图7:充斥在帧时间线上的事件导致页面闪烁

为了最大限度地减少对主线程的过度调用,Chrome会合并连续事件(例如wheel, mousewheel, mousemove, pointermove, touchmove),并进行延迟调度,直到下一个 requestAnimationFrame

图8:与上图相同的时间线,但是正在合并和延迟事件

图8:与上图相同的时间线,但是正在合并和延迟事件

任何离散事件,例如 keydownkeyupmouseupmousedowntouchstart、和 touchend 都会被立即发送。

使用 getCoalescedEvents 获取帧内事件

对于大多数Web应用程序,合并事件应足以提供良好的用户体验。 但是如果要构建一个绘图应用并根据 touchmove 坐标放置路径,则可能会在绘制平滑线时丢失中间坐标。 在这种情况下,你可以在鼠标事件中使用getCoalescedEvents方法来获取有关这些合并事件的信息。

图9:左侧是平滑的触摸手势路径,右侧是合并限制路径

图9:左侧是平滑的触摸手势路径,右侧是合并限制路径

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.
    }
});

下一步

在本系列中,我们介绍了Web浏览器的内部工作原理。 如果你从未想过为什么"开发者工具"建议在你的事件处理中添加{passive: true}或者为什么你可以在脚本标记中编写async属性,我希望本系列能够说明为什么浏览器需要这些信息来提供更快更顺畅的体验。

使用Lighthouse

如果你想让自己的代码对浏览器友好,但不知道从哪里开始,可以使用Lighthouse这个网站审计工具,它为你提供一份报告,说明正在做什么和需要改进什么。 阅读审核列表还可以让你了解浏览器关注的内容。

了解如何衡量性能

不同网站的性能调整可能会有所不同,因此,衡量网站的效果并确定最适合你网站的内容至关重要。 Chrome DevTools团队没多少关于如何衡量网站性能的教程。

向你的站点添加功能策略

功能策略是一个新的Web平台功能,可以在你构建项目时为你提供保护。 启用功能策略可确保应用的某些行为并防止你出错。 例如,如果要确保应用永远不会阻止解析,或者可以在同步脚本策略上运行应用。 启用 sync-script: 'none' 时,将禁止解析器阻止 JavaScript 执行。 这可以防止你的代码阻止解析器,并且浏览器也不需要担心暂停解析器。

总结

thank you

当开始构建网站时,我几乎只关心如何编写代码以及怎样才能帮助我提高工作效率。 这些很重要,但我们也应该考虑浏览器如何获取我们编写的代码。 现代浏览器将继续致力于为用户提供更好的Web体验。 反过来通过使代码对浏览器友好,也可以改善你的用户体验。 希望我们一起努力追求更好的浏览器!


本文首发微信公众号:jingchengyideng 点击下面链接查看其它章节文章