您现在的位置是:首页 > 正文

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

2024-01-30 21:36:05阅读 0

原文: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();
  }
}, { passive: true });

事件是否可以取消?

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

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

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

同时,你也可以用如下 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

网站文章

  • MySQL触发器

    触发器含义在发生insert,update,delete等事件时,符合特定条件后,执行特定语句即为触发器。触发器操作创建触发器create trigger 触发器名称 begin或者after 触发事件 on 表名 for each row 执行语句;create trigger 触发器名称 begin或者after 触发事件 on 表名 for each row begi

    2024-01-30 21:35:59
  • SparkSQL项目

    SparkSQL项目

    YARN产生背景MapReduce1.X的问题:JobTracker的压力太大了;YARN的产生YARN的架构1个RM(ResourceManager)+N个(NodeManager)Resource...

    2024-01-30 21:35:30
  • 基于element-ui的table实现树级表格操作及单元格合并

    基于element-ui的table实现树级表格操作及单元格合并

    基于element-ui的table,在一张表内实现多级树状数据展示及同属性的单元格合并,并在表格内实现增删改操作。

    2024-01-30 21:35:21
  • 差分算法(用python语言实现)

    差分算法,易上手

    2024-01-30 21:35:14
  • 《美图数据统计分析平台架构演进》阅读有感

    《美图数据统计分析平台架构演进》阅读有感

    《美图数据统计分析平台架构演进》阅读有感数据统计是一个比较尴尬的事情,第一个它可能不是一个非常有技术含量的事情,对于技术人员的成长来说不是非常好。第二它可能是一个比较重复工作的事,需要解决一些简单的需求的重复工作。统计业务与技术碰撞这基本上是我自己亲身的经历,刚开始一个人做这一块的业务,会碰到一些有意思的点,可能分三个阶段,第一个阶段是在项目的初期,我们是怎么样去应对一些产品的初期需求...

    2024-01-30 21:34:45
  • unity 3.6在import package时只有custom package

    unity 3.6在import package时只有custom package

    【问题描述】下载的unity 3.6在import package时只有custom package没有其他包文件【解决方案】在asset store里搜索Standard Assets选择导入即可导入完成后在左下角的project assets中多了一个Standard Assets文件夹打开即可使用...

    2024-01-30 21:34:37
  • LLCC68寄存器模式开发-几个关键操作说明

    LLCC68寄存器模式开发-几个关键操作说明

    llcc68 lora模式寄存器说明

    2024-01-30 21:34:29
  • RxJava 两种生产和消费模式,(冷)cold和(热)hot

    RxJava目前有两种发布和订阅模式。

    2024-01-30 21:34:23
  • (pytorch进阶之路)四种Position Embedding的原理及实现

    (pytorch进阶之路)四种Position Embedding的原理及实现

    定义子函数,获得每个window中两两patch之间二维的位置偏差,使用torch.meshgrid函数,根据x轴和y轴范围得到网格每个点的x坐标和y坐标,将其堆叠,获取任何两个点之间的横轴与纵轴坐标...

    2024-01-30 21:33:56
  • otter学习 | canal和otter的关系? 热门推荐

    otter学习 | canal和otter的关系? 热门推荐

    在回答这问题之前,首先来看一张canal&otter和mysql复制的类比图: mysql的自带复制技术可分成三步: master将改变记录到二进制日志(binary log)中(这些记录叫做二进制日志事件,binary log events,可以通过show binlog events进行查看); slave将master的binary log events拷贝到它的中继日志(...

    2024-01-30 21:33:49