深入理解现代浏览器 阅读笔记五

564天前 · 代码 · 1376次阅读

在上一篇博客中,我们了解了浏览器如何将开发者编写的 HTML、CSS 和 Javascript 转化成最终显示在用户屏幕上的画面。在这一过程中,我们认识到了光栅化以及合成器线程,而今天的这篇博客也与其有着密不可分的关联。

输入事件(input events)

从用户的视角来看,可能输入时间就是那些点击输入或者拖动。但是从浏览器的角度来看,用户的一切动作都可以看作是输入事件,例如鼠标滚轮滚动鼠标悬停

浏览器进程是首先捕捉到用户的输入事件的,但是浏览器只是知道用户进行了这样的一个操作,如何对用户的操作进行反应还是会交由渲染器进程处理。浏览器进程将用户的输入事件经过合成线程传递给渲染器进程,包括事件的类型(如 touchstart)和坐标,由渲染器主线程和合成线程相互配合完成对事件的响应。那么问题来了,渲染器进程是如何通过这个坐标和事件类型来判断用户到底在哪个元素交互,想要触发什么动作呢?

输入事件的处理

处理输入事件

细心的朋友可能发现了,在上一篇博客里面最后其实也涉及到了一些简单的输入书简处理的问题。用户滚动鼠标滚轮,合成器线程生成一个个合成帧最终渲染到屏幕上,这就是最基础的输入事件处理。在上篇博客的简单例子中,没有什么对于滚动的监听函数,合成器线程可以独立地完成这一次事件的处理。

当页面变得复杂,合成器线程和渲染器主线程的配合才得以体现。两者配合的大致过程如下:

浏览器进程接收到输入事件,将其坐标和事件类型传递给合成器线程后,合成器线程判断输入事件是否会触发监听回调,如果会触发就通知渲染器主线程去执行相应的 Js 函数,等到Js执行完后再去生成合成帧;如果不会触发,那么就和上面的滚动一样了,合成器不需要通知主线程,自己就可以直接渲染出一个合成帧丢给GPU。

聪明的合成器

在这个过程中,合成器如何快速判断点击到的位置是否包含有监听器呢?首先排除每一次都从DOM树上找的方案。chrome的开发者们找出了一个非常高效的方法,为每一次合成出来的页面打上记号,通过记号来快速判断。

具体来说,合成器在合成当前的页面后,会对每一个包含挂载有事件监听器的元素的区域打上一个 non-fast scrollable region 的记号。这是直接对与区域的标记,如果点击的位置不属于被标记的范围,那合成器直接自己去合成新合成帧就可以了。如果在标记的区域内,那就需要再等等 Js 的执行了。

non-fast scrollable region

辛苦的主线程

假如点击到了带有事件监听器的区域,合成线程将事件传递给了主线程。主线程在拜托 Js 执行回调函数之前,还需要先找到用户到底是在和那一个元素交互。主线程可不像合成器线程一样能走走捷径,为了准确找到目标元素,它需要先进行一次 hit test。所谓的 hit test 其实就是在渲染过程中生成的 paint records 里去找到该事件点击的坐标上的绘制顺序,最后找到被交互的目标元素。

hit test

最终,找到了目标元素的渲染器主线程将各种信息(坐标、目标元素、事件类型)等打包成我们熟知的 event 对象传递给开发者定义的回调函数去执行。如果执行的过程中改变了DOM,合成器继续等,等到重新渲染出新的 Layout tree 和 paint records 来合成新的合成帧。如果没有改变DOM,那就直接渲染就好啦。

事件合成

在之前的博客中,我们讨论过为了让用户有更加流畅的观感,以普通的显示为例,浏览器每秒交错刷新60次。但是对于输入事件来说,一般的触屏设备每秒可以分发60~120次的触摸事件,一般的鼠标可以分发100次左右的事件。可以看得出来,用户输入的事件的频率是远超我们的屏幕刷新次数的。

考虑一个类似 touchmove 的持续输入事件,浏览器如果不经任何处理,每秒可以将差不多120个事件塞到渲染器主线程去进行一次又一次的 hit test,这远远超出了屏幕刷新的速度。

太多事件啦

想到这里,可能有些经验老道的朋友就想到了,我们可以利用节流来限制呀。就像例如点赞等一些经典场景,如果前端一监听到有人按下点赞按钮就向后端发送请求,如果碰上一群手速怪人,服务器可不好受。前端工程师为了防止服务器被这些人 gank,就心生一计,把一段时间内的点赞合成一个请求发出去,就算用户点得再快,也不过是按照我们预设的频率请求。

浏览器工程师直呼“英雄所见略同”。为了让渲染器进程不会被用户随便轻轻一拉就冲垮,他们将一段连续的事件合并到一起发送给渲染器进程。当我们不太关注用户手指滑动的路径的时候,其实在一段时间内的滑动最主要位置的也就是起点和终点的坐标。通过合并这一操作,渲染器进程处理用户输入就变得相对游刃有余了。

叠放,召唤巨大(不是

在优化后,我们可以发现合并后的时间派发给渲染器进程的时间点都在下一次 requestAnimationFrame 之前,确保能在之后执行的 Js 中获取到。

一般来说只有那些连续的事件会被合并,例如 wheel, mousewheel, mousemove, pointermove, touchmove 。而像 keydown, keyup, mouseup, mousedown, touchstart 这种事件则会被立即派发到渲染器进程。

可是问题也来了,如果我们确实很关心用户滑动的路径呢,例如我想实现一个在线画板之类的东西。按照上面的逻辑,用户的画都变成一个个线段了。这可咋整呢?

getCoalescedEvents

浏览器工程师也想到了这一点,并为开发者们开放了一个接口 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.
    }
});

性能优化?

在我们知道了浏览器是如何处理用户的输入事件之后,我们也许可以去思考一下如何让我们的代码能更加充分地利用浏览器的一些特性。或者至少不会拖慢浏览器应有的速度。

事件委托

事件委托是现代 Js 编程中常见的处理事件的方式。考虑一个列表 <ul> ,其中有很多的列表项 <li> 。你想为其中每一个列表项都添加一个点击事件,如果为每一个 <li> 都加上一个 onclick 无疑会对浏览器造成不小的负担。而如果按照事件委托的方式去处理我们的需求,利用事件冒泡的机制,我们可以将 onclick 挂在 <ul> 上,子元素的点击事件都会被父元素的这个 onclick 捕捉到,在父元素的回调函数内加上一个简单的判断,就可以实现相同的效果了。

从开发者的角度来看,这无疑非常方便,既减轻了工作量,又减轻了浏览器的工作负担,岂不美哉。确实,在上面的例子来看,这样子优化确实非常有效。但是考虑下面的场景。

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

不知道出于什么样的精神状态,一位开发者给 body 挂上了事件监听器。这个时候合成器线程傻了,好家伙,但凡这页面有关于 touchstart 的一点风吹草动都得去问问主线程。主线程也傻了,每次都去找一遍目标元素,交给 Js 引擎去执行回调函数。回调函数一看,😯,不是我想要的元素,大家就当无事发生。合成器听了沉默,主线程听了流泪。

img

在这种情况下,合成器线程的优化一点效果没有不说,甚至完全就是负作用。

幸好要解决这个问题还算比较简单,只需要在挂载监听器的时候加上 passive: true 参数即可。

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

这个参数用于提示浏览器,我虽然这么监听,但是可能90%的触碰都是无效的,你合成器进程不用管我,自己合成就好。

This hints to the browser that you still want to listen to the event in the main thread, but compositor can go ahead and composite new frame as well.

试试 Lighthouse

在读完了现代浏览器系列的四篇博客之后,你应该已经对浏览器的运行原理有了一个大体的了解。对什么东西会影响到网页的性能可能也有了自己的认识。如果想要优化自己的页面,lighthouse 可以给你提出一些针对性的建议,快打开开发者工具去试试看吧!

惨淡的 lighthouse 评分

结语

《深入理解现代浏览器》系列的读书笔记到这里就结束了。这几篇博客给我带来的知识感觉超越了其本身的内容,更重要的是它将我之前对于前端一些知识点的错误理解以及一些疑惑都进行了一个改正与点拨,让我真真切切地有了一种豁然开朗的感觉。果然学习还是要深入原理才可以呀。

👍 8

浏览器 前端

还没有修改过

评论

取消回复
贴吧 狗头 原神 小黄脸
收起

贴吧

  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡
  • 贴吧泡泡

狗头

  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头
  • 狗头

原神

  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神
  • 原神

小黄脸

  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  • 小黄脸
  1. 蓝易云 502天前

    您好,博主
    可以投稿服务器或者scdn文章内容吗?
    这边可以赞助高防scdn【国内外均可】【一年起】
    并且会给您一个心意红包
    国内正规持证公司

    如果不方便发您的联系方式

    以下是我的联系方式!

    我的QQ是:2814841448

  2. 辞惺. 523天前

    太好看了吧

  3. Kiosr 525天前

    讲道理,我也是第一次看到有目录

    1. 季悠然 525天前

      类目了 lei

  4. akunqaq 540天前

    博客主题新增了目录呀?真棒! huaji_xiao

    1. 季悠然 539天前

      其实一直都有,只不过开关比较隐蔽 kuanghan

目录

avatar

季悠然

寻找有趣的灵魂

139

文章数

2069

评论数

3

分类

好热啊

arknights!


Warning: file_get_contents(https://v1.hitokoto.cn?encode=json&c=d&c=b): failed to open stream: HTTP request failed! HTTP/1.1 403 Forbidden in /www/wwwroot/blog.mitsuha.space/C/themes/G/components/widgets.php on line 47

1484