Inside look at modern web browser (part 4)
原文地址:https://developers.google.com/web/updates/2018/09/inside-browser-part4
用户输入进入合成器
这是了解 Chrome 如何处理您的代码以显示网站内部系列博客的最后一篇。在上一篇文章中我们了解了 Chrome 的渲染过程和合成器。本文我们将了解合成器如何实现流畅的用户交互。
从浏览器的角度看用户输入
当您听到“输入事件”时,您可能只会想到键入文本框或单击鼠标,但是从浏览器的角度来看,输入包含用户的所有手势。鼠标滚轮滚动是输入事件,触摸或鼠标悬停也是输入事件。
当用户触发手势(如屏幕上的触摸)时,浏览进程将首先接收手势。但是,浏览进程仅知道该手势发生在哪里,因为 Tab 页的内容由渲染进程处理。因此,浏览进程将事件类型(如 touchstart
)及其坐标发送给渲染进程。渲染进程通过找到事件目标(EventTarget)并运行附加的事件监听器来响应输入。
图 1:输入事件从浏览器进程传递到渲染进程合成器接收输入事件
在上一篇文章中,我们研究了合成器如何通过合并栅格化图层来平滑处理滚动。如果该页面上没有任何输入事件监听器,则合成线程可以创建一个完全独立于主线程的新合成帧。但是,如果将某些事件监听器附加到页面上怎么办?合成线程如何判断是否需要处理事件?
理解非快速滚动区域
由于 Javascript 是在主线程上运行,因此在合成页面时,合成线程会将页面上具有事件处理程序的区域标记为“非快速滚动区域”。有了这些信息,如果事件发生在该区域中,则合成线程可以确保将输入事件发送到主线程。如果输入事件来自该区域之外,则合成线程将在不等待主线程的情况下进行新帧的合成。
图 2:非快速滚动区域发生输入事件的示意图编写事件处理程序时的注意点
事件委托(事件代理)是一种 Web 开发中常见的事件处理模式。由于事件冒泡,您可以在最顶层的元素上附加一个事件处理程序,并根据 EventTarget 委派任务。您可能看到过或编写过如下代码:
document.body.addEventListener('touchstart', event => {if (event.target === area) {event.preventDefault();}
});
由于您只需要为所有元素编写一个事件处理程序,因此事件委托模式的功效是很吸引人的。但是,如果从浏览器的角度查看此代码,现在整个页面都被标记为不可快速滚动的区域了。这意味着,即使您的页面的某些部分不关心输入事件,合成线程也必须与主线程通信,并在每次输入事件发生时等待主线程。此时,合成线程就丧失了使页面平滑滚动的能力。
图 3:整个页面都是非快速滚动区时,输入事件的处理示意图为了避免这种情况的发生,您可以在事件监听时设置 passive: true
。这会告知浏览器您仍要在主线程中监听事件,但是合成器也可以并行继续合成新的帧。
document.body.addEventListener('touchstart', event => {if (event.target === area) {event.preventDefault()}}, { passive: true });
检查事件是否可以取消
假设您在页面中有一个框,想将滚动方向限制为水平滚动。
在手势事件中使用 passive: true
可以让滚动流畅,但垂直滚动可能在你调用 preventDefault()
来阻止垂直滚动之前就开始了。您可以使用 event.cancelable
进行检查。
图 4:页面中的部分区域固定为水平滚动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;
}
查找 EventTarget
当合成线程将输入事件发送到主线程时,要做的第一件事是运行命中检测以找到 EventTarget
。命中检测使用在渲染阶段中生成的绘制记录来找出事件发生的点坐标对应的元素。
图 5:主线程使用绘制记录查找在 x, y 的位置是什么元素压缩分配到主线程的事件
在上一篇文章中,我们讨论了如何每秒刷新屏幕 60次,以及如何保持刷新率以实现流畅的动画。对于用户输入,常用的触摸屏设备每秒发送 60-120次触摸事件,而常用的鼠标则每秒发送 100次事件。输入事件的频率高于我们的屏幕刷新能力。
如果诸如 mousemove
这样的连续事件被以每秒钟 120次的频率传递给渲染进程的主线程,与屏幕低刷新率相比,会触发过量的命中测试和 Javascript 的回调执行。
图 6:事件泛滥到帧时间轴上,导致页面卡顿为了尽量减少对主线程过度调用,Chrome 的聚合了连续事件(如 wheel
,mousewheel
,mousemove
,pointermove
, touchmove
)并延迟调度直到下一次 requestAnimationFrame
执行前。
图 7:与之前相同的帧时间轴,但事件被合并并延迟执行非连续事件,如 keydown
,keyup
,mouseup
,mousedown
,touchstart
,和 touchend
会被立即派发。不会进行延迟和合并。
(译者注:这也是为什么当使用扫码枪一类的输入设备时,某些键盘上正常使用的输入框,可能不再可用,特别是在使用受控方式编写 format 逻辑的情况下。)
使用 getCoalescedEvents
得到帧内事件
对于大多数 Web 应用程序,合并事件足以提供良好的用户体验。但是,如果要构建诸如画图应用程序之类的基于 touchmove
坐标路径的东西,则可能会丢失中间的坐标,而使绘制不平滑。
图 8:左侧是手势路径,右侧是合并结果路径,变得不平滑window.addEventListener('pointermove', event => {const events = event.getCoalescedEvents();for (let event of events) {const x = event.pageX;const y = event.pageY;// 使用 x、y 坐标绘制路径}
});
结语
在本系列中,我们介绍了 Web 浏览器的内部工作原理。如果您从未想过为什么 DevTools 建议添加 { passive: true }
给事件处理程序,或者为什么要添加 async
在
标签中,那么我希望本系列文章能阐明为什么浏览器需要这些信息来提供更快,更流畅的 Web 体验。
总结
当我开始建立网站时,我几乎只关心如何编写代码以及什么可以帮助我提高效率。这些事情很重要,但是我们还应该考虑浏览器如何使用我们编写的代码。现代浏览器正持续为用户提供更好的 Web 体验而投入资源。通过组织对浏览器友好的代码,从而改善了您的用户体验。希望您加入我们的行列,以求对浏览器友好!
译者额外补充
Chrome v51 开始引入 { passive }
。Chrome v55 开始 touchstart
and touchmove
默认 passive
为 true
。Chrome v73 增加了 wheel
、mousewheel
事件默认 passive
为 true
。
Pegasus:[翻译] 瞧一瞧现代游览器如何工作?Part 1zhuanlan.zhihu.com
Pegasus:[翻译] 瞧一瞧现代游览器如何工作?Part 2zhuanlan.zhihu.com
Pegasus:[翻译] 瞧一瞧现代游览器如何工作?Part 3zhuanlan.zhihu.com