深入了解现代网络浏览器(第 4 部分)

194 阅读4分钟

「这是我参与2022首次更文挑战的第29天,活动详情查看:2022首次更文挑战

当用户输入时,合成器是如何实现流畅的交互。

从浏览器的角度输入事件

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

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

合成器接收输入事件

如果页面上没有附加输入事件侦听器,则合成器线程可以创建一个完全独立于主线程的新合成框架。但是,如果某些事件侦听器附加到页面怎么办?合成器线程如何确定是否需要处理事件?

了解非快速滚动区域

由于运行 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 });

寻找事件目标

当合成器线程向主线程发送输入事件时,首先要运行的是命中测试以查找事件目标。命中测试使用在渲染过程中生成的绘制记录数据来找出事件发生的点坐标下方的内容。

最小化事件分派到主线程

对于输入,典型的触摸屏设备每秒传递 60-120 次触摸事件,而典型的鼠标每秒传递 100 次事件。输入事件具有比我们的屏幕刷新更高的保真度。

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

为了尽量减少对主线程的过多调用,Chrome 会合并连续事件(例如 wheelmousewheelmousemovepointermovetouchmove)并将调度延迟到下一个requestAnimationFrame

任何离散事件,如keydownkeyupmouseupmousedowntouchstart和都会touchend 立即分派。

用于getCoalescedEvents获取帧内事件

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

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