Inside look at modern web browser(4) -- 浏览器如何处理交互事件

508 阅读7分钟

最终一章将花些时间看看用户和网页交互时,浏览器做了什么事情

从浏览器的视角看交互

  • 这里我没有直译input event,而是翻译成了交互,道理是这里作者想要讨论的是任何用户发出的可以被浏览器捕获的event,而如果直译为输入事件会造成歧义,所以干脆就翻译成交互了
  • 当一个交互事件发生的时候,浏览器进程会首先判断出事件的类型和位置,然后这个事件被发送给了渲染进程,渲染进程会找到这个事件的目标,然后运行注册好的响应事件函数

合成线程接受交互事件

  • 原文这里没有内容只是提了一个问题,我们可以思考一下。上一篇文章说到合成线程的作用是合成栅格化的layer,那么如果当前页面没有注册任何在页面上的事件,合成线程完全可以独立于主线程完成合成的工作,因为在滚动的过程中其实主线程是处在挂起的状态,不会做任何的事情(这里不考虑任何动画的,或者样式的更改);但是如果有事件注册在了当前页面呢,那么合成线程如何知道一个交互事件发生的时候是否有注册的回调函数要执行的呢?

⚠️理解非快速滑动区域⚠️

  • 因为javascript是被主线程执行的,那么当当前页面被合成的时候,合成线程会标记有事件注册的区域为非快速滑动区域,那么有了这个信息,当有交互事件发生在这个区域的时候合成线程就会把这个信息给到主线程,让主线程处理。反之,如果出了非快速滑动区域,那么合成线程直接合成而不等待主线程。
  • ❗️TBD,这里加点儿我自己的理解说明一下为什么一旦有事件注册的话合成线程要等待主线程,举个极端一点儿的例子,如果交互事件的回调函数直接改变了文档结构,那么这个时候合成线程不等待主线程的结果直接合成了,但是因为文档结构改变了,那么上篇文章说的渲染流水线的整个过程都要走一遍,那么刚刚合成过的就没有意义了,当然这只是我根据文章内容的推断,如有❌请指出❗️

注意注册交互事件的方式

  • document.body上直接注册事件回调处理函数,然后在回调函数内部去判断事件的目标是,即常用的事件委托模式(因为事件是冒泡的),导致的结果是什么呢,整个页面都是非快速滑动区域,因此每一次合成都需要等主线程再合成,大大降低了合成的效率,那么可以通过在addEventListener中加入{passive: true}来告诉合成事件可以直接合成,而不用等主线程,淡然主线程中的事件回调函数还是会被执行的,只是在合成时不等主线程了。

检查交互事件是否可以被取消

  • 有时你可能想要页面只可以水平滑动,而不允许垂直方向的滑动,但是这里有个吊诡的地方,利用平常的方式一定会这么写
document.body.addEventListener('pointermove', event => {
	event.preventDefault()
}, {passive: true}); //如上所述,这个是因为要保证合成的流畅

但是只有在触发这个事件时才会被调用,也就是已经滚动了,然后禁止,作者提供了如下的方法

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

即利用事件的cancelable来取消事件,当然也可以利用css touch-action: pan-x;实现

确认事件的目标

  • 当合成线程把这个交互事件给到主线程,第一件是就是run一个hit test来找到事件的目标,这个hit test是利用上一篇讲到过的绘画记录,绘画记录主要是关于垂直于浏览器的信息的,那么就可以通过事件发生的位置和绘画记录来确定事件的目标主体

❗️最小化事件派送到主线程的次数❗️

  • 上一篇讨论过浏览器的刷新率,以及可能导致卡帧掉帧的情况,那里讲到了正常浏览器的刷新率是60fps,这是图像输出,如果讨论交互事件,那么现在的主流触屏的采样率都是60-120hz,这是输入,也即是设备每秒会采样60-120次,可以看出,采样率要高于刷新率,这样做的目的是为实现对交互事件的高保真,即保证设备对交互事件采集的尽量完整
  • 如果是连续的事件,比如touchmove,当然这里讨论的是当前页面针对次事件有注册了的回调函数,采样率比如是120hz,这时触发touchmove一秒,意味着浏览器在这一秒内将接到120次交互事件,这所有的事件也都会被给到合成线程,当然也会从合成线程给到主线程,根据前边知道,每次合成都要等主线程的回调执行,所以造成一个问题,就是因为这一秒的touchmove,导致主线程执行了120次回调,而且每次执行前在主线程中都需要run hit test来确定事件目标❗️而他们全部都在主线程上发生
  • 这会导致一个什么问题呢。还是那这个例子,重新梳理一下,浏览器的刷新频率是60fps,也就是如果有动画效果每合成一帧最晚也需要在上一帧完成后的16.6ms之内(这个上一篇文章讨论过了),但是因为上一段说的采样率过高的原因导致这一秒内会触发120次hit test和回调函数的执行,发生的频率是刷新的两倍,又因为他们都在主线程上,所以16.6ms之内要完成2次hit test和两次回调函数的执行,然后还要给够合成线程足够的时间来合成,很可能事件不够,那么就会出现掉帧的情况,从而导致卡顿。
  • 当然我们要避免写这样的event listener,同时Chrome从底层也在帮助我们进行优化,它所做的就是对连续的事件进行合并,然后延迟发送,直到下一次的requestAnimationFrame也就是主线程有空了再发送交互事件;对于离散的交互事件,会马上发出

getCoalescedEvents 来获取在帧内的事件

  • 上述的合并事件对于很多的应用来说是好的优化手段,但是如果是类似画画工具的应用,不希望丢失所有采集到的事件,因为那样可能丢失了很多的位置信息,这时候就需要getCoalescedEvents方法来获取那些被合并的事件信息了

完结

  • 到此这个系列的文件就终结了,作者带我们了解了现代浏览器的基本架构,回答了一个经典的问题,即从输入URL到页面显示的整个流程,页面是如何被渲染的以及这篇浏览器是如何处理交互的,这些文章至少对于我来说解了很多的惑,希望也能给读者带来一样的感受。

本节refs:

上一节

Inside look at modern web browser(3) -- 一个tab中的网页渲染全过程