现代浏览器内部机制 Part 4 | 事件

程序员鱼皮

共 5007字,需浏览 11分钟

 ·

2021-03-19 17:21

原文:Inside Look at Modern Web Browser(part 4)[1]

作者:Mariko Kosaka[2]

译者:kyrieliu

终于到最后一篇了!作为这个系列的最后一篇文章。在之前的文章中,我们了解了现在浏览器的多进程架构导航以及渲染进程和合成器。在这篇文章中,我们将了解到合成器是如何在用户输入时流畅的处理交互的。

从浏览器的角度定义输入事件

当提到“输入事件”时,你可能会想到在文本域中打字或是鼠标的点击事件,但在浏览器看来,用户的任何动作都意味着“输入”。鼠标滚轮的滚动是一种输入事件,触摸或者鼠标滑过也是一种输入事件。

当用户的交互行为发生时(比如触摸点击屏幕),浏览器进程会第一个感知到这个用户行为,但也仅仅是感知而已,因为浏览器 tab 下的内容都是由渲染进程全盘掌控着。于是浏览器进程在第一时间将用户事件的类型和坐标发送给渲染进程。渲染进程通过查找并调用对应的事件处理函数来处理这个用户输入事件。

合成器接收到输入事件

在上一篇文章中,我们研究了合成器如何通过光栅化图层来平滑的处理滚动。如果页面上没有事件监听器,合成器线程会创建一个完全独立于主线程的新的合成帧。如果页面上挂在了一些事件监听器又会发生什么呢?合成器线程又是怎样找出需要被触发的事件呢?

非快速滚动区域

因为运行 JavaScript 是主线程的任务,当一个页面被合成,合成器线程将页面上挂在了事件处理器的区域标记为“非快速滚动区域”。有了这个标记之后,合成器就能保证在对应的区域触发输入事件时可以向主线程传递这一事件。如果输入事件来自于这个区域之外,合成器则会持续合成新的帧,并不会等待主线程。

写事件处理器时要注意

在 Web 开发中一个比较常见的事件处理模型就是事件委托(代理)。因为事件的冒泡机制,开发者可以在最顶层的元素挂载一个事件处理函数,并且基于 event target 分发不同的处理逻辑。下面的代码,你可能已经司空见惯了:

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

只用写一个事件处理器就可以搞定所有的输入事件,这在工程学上是一件极具魅力的事情。当你从浏览器的视角审视这段代码的时候,你会发现整个页面都被标记成了“非快速滚动区域”。这就意味着即使你的 web app 不关心来自页面上某个位置的输入事件,但合成器线程仍然会基于这次触发的事件和主线程进行“交流”。在这种模式之下,合成器本身“平滑处理页面滚动”的能力就不复存在了。

为了减轻这种情况的发生,开发者可以给自己的事件处理器传递 passive: true 这样一个参。这等同于告诉浏览器开发者仍然希望在主线程中监听页面上每一次触发的输入事件,但也希望合成器该干啥干啥,持续合成新的帧。

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

事件是否可以取消?

假设此时页面上有个容器,你只想让它进行水平滚动。

在你的鼠标事件监听函数中使用 passive:true 意味着页面的滚动可以按照往常纵享丝滑般地去处理,你会为了限制滚动的方向调用 preventDefault ,但在这之前竖直的滚动就可能已经发生了。你可以通过 event.cancelable 针对此种情况进行相应的优化。

document.body.addEventListener('pointermove', event => {
  if (event.cancelable) {
    event.preventDefault();
  }
}, { passivetrue });

同时,你也可以用如下 css 来帮忙消除事件处理器:

#area {
  touch-action: pan-x;
}

查找 event target

当合成器线程向主线程发送了一个输入事件后,第一件事情就是通过 hit test(点击测试) 找到对应的 event target(事件目标,还是不翻译这个词比较正宗)。Hit test 利用渲染进程产生的绘制记录来找出在触发本次输入事件的坐标底下的真实元素。

减少主线程的事件处理负担

在上一篇文章中,我们讨论了主流的显示器通过每秒 60 次的频率刷新以及我们需要跟上这个节奏以实现流畅的动画效果。对于输入事件来说,主流的触摸屏会以每秒 60 到 120 次的频率向主线程传递触摸事件,大多数的鼠标事件都被以每秒 100 次的频率传递给主线程。输入事件的保真度是普遍高于主流屏幕的刷新能力的。

如果一个持续不断的事件(比如 touchmove)在一秒内被传递给了主线程 120 次,这就会触发大量的 hit test 和 JavaScript 的执行,这么一对比,每秒 60 次的屏幕刷新速率就显得太慢了。

为了减少主线程的负担,Chrome 将常见的连续事件进行了合并(比如 wheel、mousewheel、mousemove、pointermove、touchmove 等),并且在 requestAnimationFrame 中延缓了事件的触发时机。

其他“分散触发”的事件(keydown、keyup、mouseup、touchstart、touchend 等)仍保持立即触发的策略。

通过 getCoalescedEvents 获取帧内事件

对于大多数的 web app 来说,合成事件是为了更好的用户体验。假如你在开发一款绘画的应用程序,如果你根据 touchmove 的坐标来放置路径,大概率是会丢失掉中间的坐标的,你也就无法画一条平滑的线了。这种情况下,你就可以用 getCoalescedEvents 这个方法来获取更多关于合成事件的信息。

window.addEventListener('pointermove', event => {
  const evnets = event.getCoalescedEvents();
  for (let event of events) {
    const x = event.pageX;
    const y = event.pageY;
    // 用这些坐标画线,稳
  }
});

接下来...

在这个系列中,我们详细的探讨了现代浏览器的内部工作机制。如果你之前从来没有想过为什么官方推荐在你的事件处理函数中添加 passive 参数,或者不知道为什么在 script 标签上添加 async 属性,我希望这个系列能为你阐明为什么浏览器需要这些东西来提供更快、更流畅的用户体验。

Lighthouse 用起来

如果你想让自己的代码变得更加“浏览器友好”却不知道从哪里开始,不妨试试 Lighthouse[3] 吧。Lighthouse 是一个可以对网站进行审核检查的工具,会为开发者提供一份包含网站得分以及优化方案的详尽报告。

学习如何度量性能

不同的网站对于性能的需求可能不同,因此找到合适的度量方法以及优化方案是至关重要的。Chrome 的开发者工具团队有话说:通过 Chrome Devtools 优化网站性能[4]

给网站添加 Feature Policy

如果你想更进一步,Feature Policy 了解一下?Feature Policy 是一个新的 web 特性,它可以在开发者构建 web app 时提供“保护”。启用 feature policy 可以确保你的 web app 具备某些行为,并在一定程度上避免开发者犯错。举个例子,如果你希望保证你的 app 不会阻塞解析,你可以在同步脚本策略之下运行你的 app。当 sync-script:none 打开时,会阻塞解析的 JavaScript 都会被阻止执行。这一策略会防止任何“脚本阻塞解析”的发生,浏览器就再也不用担心解析被阻塞这件事情了。

总结

当我在构建网站时,我通常只关注怎么写代码以及怎样才能让自己的效率变得更高。这些事确实很重要,但我们也需要关注浏览器究竟会怎样处理我们的代码。现代浏览器在持续地为用户提供更好的 Web 体验。通过组织我们的代码对浏览器更加友好,也能改善用户体验,可谓一举两得一石二鸟一箭双雕!

参考资料

[1]

Inside Look at Modern Web Browser(part 4): https://developers.google.com/web/updates/2018/09/inside-browser-part4

[2]

Mariko Kosaka: https://developers.google.com/web/resources/contributors/kosamari

[3]

Lighthouse: https://developers.google.com/web/tools/lighthouse

[4]

通过 Chrome Devtools 优化网站性能: https://developers.google.com/web/tools/chrome-devtools/speed/get-started




本文来自刘凯里,专注分享有趣的前端知识。刘哥还为大家准备了前端面试官手记,在他的公众号回复【前端面试】即可获取,欢迎大家的关注~


往期推荐

现代浏览器内部机制 Part 1 | 多进程架构

现代浏览器内部机制 Part 2 | 导航这件小事

现代浏览器内部机制 Part 3 | 渲染进程的一生

浏览 47
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报