Hi 大家好,这里是 探索现代浏览器 专栏的第四篇。
在前面的探索历程中,我们知道了有个概念叫做渲染流水线
,我们也知道了浏览器是如何渲染一个页面的。
我们都知道,浏览器之所以在互联网中占据如此重要的地位,除了渲染页面之外还有一个更为重要的能力:那就是能处理用户交互。今天就一起来深入探索一下:
浏览器是如何处理用户输入的?
浏览器进程接收输入事件
我们所理解的用户输入事件,一般是用户在页面输入框输入内容,或者点击某个按钮。但是对浏览器而言,用户所有的手势操作 都叫做输入,包括但不限于:
- 页面滚动
- 触碰屏幕
- 移动鼠标
- ……
当我们触发输入事件时,浏览器首先会进行如下处理:
浏览器进程(Browser Process)
会首先接收到该事件。但浏览器进程
只能知道该输入事件发生在什么地方,并不知道如何处理,因为页面内容是在渲染进程(Renderer Process)
里处理的浏览器进程
会将 事件类型(比如 touchstart) 以及 坐标 发送给渲染进程
渲染进程
会找到事件的目标对象,并执行事件绑定的监听函数
合成线程接收输入事件
浏览器进程会将输入事件发送给渲染进程,而在渲染进程中,首先是由合成线程来接收这个事件的。
前面 我们已经了解了合成线程是如何通过合成和光栅化来确保页面的流畅体验的。
如果当前页面没有任何监听事件,那么合成线程是完全不需要主线程来参与,就可以创建一个新的合成帧来更新页面的。
那么当页面中存在监听事件时,合成线程是如何判断是否需要将这个事件交给主线程处理的呢?
非快速滚动区域
JS 脚本是在 主线程(Main Thread)
中执行的。当一个页面被合成的时候,合成线程(Compositor Thread)
会将注册了监听事件的页面区域标记为非快速滚动区域。
当用户事件发生在这些区域时,合成线程
就会将输入事件发送给 主线程
处理。反之,合成线程
就无需 主线程
的参与来生成一个新的合成帧。
更合理的事件监听
web 开发中很常见的一种事件监听就是事件委托。
由于事件会冒泡,只需要在顶层元素绑定一个监听事件就可以作用于所有元素,这种写法看起来似乎很方便。
document.body.addEventListener('body', e => {
e.preventDefault()
})
但实际上从浏览器的角度来看,给 body 绑定了监听事件,这意味着 整个页面都变成了非快速滚动区域。也就是说当用户输入事件触发时,尽管触发事件的区域不需要关心用户的输入,但 合成线程
依然每次都会通知 主线程
并等待主线程处理完输入事件,这就丧失了合成线程的优势了,同时也会损失一定的用户体验。
为了减轻这种情况,可以在监听事件中设置 passive: true
,这个属性会告知浏览器我们仍需要在主线程中进行事件监听,但是合成线程可以继续合成新的帧,无需等待,确保流畅的滚动体验。
document.body.addEventListener('body', event => {
e.preventDefault()
}, { passive: true })
从 MDN 上的解释我们可以知道,加上该属性后浏览器默认你的监听事件不会执行 preventDefault
,也就是不会对用户正常的事件输入响应造成影响。当然,目前大部分主流浏览器在一些连续事件(鼠标移动等)都默认该参数为 true 了。
查找事件目标对象
主线程
接收到 合成线程
发送的输入事件后,第一件事就是查找事件对应的目标对象。
这个查找过程叫做 hit test,具体查找方式是:主线程
遍历渲染流水线中生成的 绘制记录(Paint Records)
,找到触发输入事件的 x,y 坐标 对应的是绘制记录中的哪个对象。
查找到目标对象之后,主线程就会执行相应的事件处理逻辑。
事件处理与渲染更新
在前面渲染流水线章节的 绘制 部分中,我们有提到显示器的刷新频率以及如何实现流畅的动画。
对于用户输入事件,一般触屏事件是每秒触发 60 – 120 次,鼠标事件是每秒触发 100 次。
这意味着像 touchmove、mousemove 这类连续触发事件,很大可能触发 hit test 和执行 JS 的频率要超过屏幕的刷新频率。这种情况下就会给用户造成页面卡顿的效果。
最小化事件派发
为了最大程度地减少对主线程的过多调用,现代浏览器会合并连续事件(例如wheel
,mousewheel
,mousemove
,pointermove
,touchmove
),并将调度延迟到下一个 requestAnimationFrame
之前。
而对于非连续事件,比如 keydown
, keyup
, mouseup
, mousedown
, touchstart
, touchend
等,则会立即派发给主线程并执行处理。
当然,如果你就是想要获取用户每一次触发的连续输入事件来做一些处理,比如画一个鼠标移动路径动画之类的,也是有办法的。可以通过 getCoalesecedEvents
来获取被合并的事件的详细信息。
举个例子:codesandbox.io/s/wonderful…
渲染更新
主线程
在完成事件处理之后,如果该输入事件引起页面内容的变化(比如文本输入、元素位置改变等),那么 渲染进程
就会发起一次新的渲染来完成页面视图的更新以及用户交互的更新。
同时,在处理输入事件时,渲染进程 也可能会与 浏览器进程 进行交互,比如发送请求,获取资源等操作。
至此,就是浏览器处理用户输入的总体流程了。
写在最后
探索完了浏览器处理用户输入的基本原理,相信大家也多多少少有一些新的收获。
如果大家对于这方面有更深入的理解,也欢迎在评论区随时交流~