原文:https://developers.google.com/web/updates/2018/09/inside-browser-part4

这篇文章,主要探讨 compositor 在有用户输入的情况下怎么实现交互平滑。

浏览器角度看输入事件

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

当发生屏幕上的触摸的用户手势时,浏览器 process 是首先接收手势。因为选项卡内部的内容由渲染 process 处理,浏览器 process 只知道该手势发生的位置。所以,浏览器 process 将事件类型(如touchstart)及其坐标发送到渲染 process 时。渲染 process 通过查找事件目标并运行附加的事件回调函数来适当地处理事件。
input.png

Compositor 接收输入事件

在上一篇文章中,我们研究了合成器(Compositor)如何通过合成栅格化图层来平滑地处理滚动。如果没有输入事件绑定到页面,则Compositor thread 可以创建完全独立于主 thread 的新复合帧。但是如果有某些事件绑定到页面上呢?合成器 thread 将如何找出事件并处理呢?

理解非快速可滚动区域

运行 JavaScript 是主 thread 的工作,因此当合成页面时,合成器 thread 标记页面的一个区域,该区域将事件回调函数标记为“非快速可滚动区域”。通过获取此信息,合成器 thread 可以确保在该区域中发生事件时将输入事件发送到主 thread 。如果输入事件来自该区域之外,则合成器 thread 在不等待主 thread 的情况下继续合成新帧。
nfsr1.png

绑定事件时要主要

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

  1. document.body.addEventListener('touchstart', event => {
  2. if (event.target === area) {
  3. event.preventDefault();
  4. }
  5. });

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

为了减少这种情况发生,您可以在监听事件时使用 passive: true。这向浏览器指示我们仍然希望在主 thread 中监听事件,但是合成器也可以继续并合成新帧。https://developer.mozilla.org/zh-CN/docs/Web/API/EventTarget/addEventListener

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

事件 target

当合成器 thread 向主 thread 发送输入事件时,首先要查找事件目标元素。使用在渲染过程中生成的绘制记录数据来找出点坐标下面的事件。
hittest.png

最小化事件派发到主线程

在上一篇文章中,解析了显示器如何每秒刷新60次,以及如何获得流畅的动画效果。对于输入,典型的触摸屏设备每秒提供60-120次触摸事件,而典型的鼠标每秒提供100次事件。输入事件具有比我们的屏幕刷新更高的保真度。
如果 touchmove 主 thread 发送120次连续事件,那么与屏幕刷新速度相比,它可能会触发多次 JavaScript 执行。
rawevents.png

为了尽量减少对主 thread 的过度调用,Chrome 会 coalesce(聚合) 连续事件(如 wheel,mousewheel,mousemove,pointermove, touchmove)和延迟调度直到下一个requestAnimationFrame。
coalescedevents.png

任何 discrete 事件,如keydown,keyup,mouseup,mousedown,touchstart,和touchend 被立即派发。

使用getCoalescedEvents得到帧内事件

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

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