在浏览器看来,用户输入包括:
- 在输入框中输入文字
- 点击鼠标
- 滚动鼠标滚轮
- 移动鼠标,或者手指在屏幕上滑动
- ...
当这些事件发生时,浏览器进程是最先收到消息的,但是,浏览器进程只知道发生的事件类型以及事件发生的坐标,因为网页内容是渲染器进程在管,所以浏览器进程会把事件类型(event type)和事件发生的坐标传送给渲染器进程,渲染器进程再找到事件目标(event target)并触发事件处理函数(event handler)。
如果用户触发了页面滑动事件,浏览器进程会把事件信息发送到渲染器进程中的合成器线程,合成器线程的处理分两种情况:
-
如果页面上没有绑定事件处理函数,那么合成器线程只需要把已经栅格化的图层合成一个新的合成器帧就可以了,这样整个滑动过程会非常丝滑。
-
如果页面绑定了事件处理函数,合成器线程会通知主线程来执行相应的事件处理函数。
当合成器线程在合成页面时,它会把页面中有绑定事件处理函数的区域标记为“非快速滑动区域”,如果用户输入事件发生在这些区域,合成器线程会通知主线程去处理这些事件,否则合成器线程就可以不用和主线程通信,直接合成新的合成器帧即可。
那么问题来了,我们在绑定事件处理函数的时候,常常会使用事件代理模式,试想一下我们把整个页面的事件处理都交给 <body>
元素来代理监听,那整个页面都会被合成器线程标记为“非快速滑动区域”,那合成器线程的丝滑滑动优点就发挥不了作用了,现在每次页面滑动时合成器线程都得先和主线程通信并等待主线程的回应。
为了合成器丝滑滑动和事件代理这两个优点能共存,我们可以在监听事件的时候传递第三个参数 { passive: true }
,意思是,主线程还是照样监听事件,但合成器线程就不用等待主线程的回复了,可以在通知主线程之后直接开始合成新的合成器帧。
当主线程从合成器线程那里接收到事件信息之后,它要做的第一件事情就是命中测试(hit test),找到事件对象,命中测试会使用在渲染过程中生成的绘制记录(paint record)来找到在事件发生坐标的具体是哪个元素。
一般触屏设备每秒会触发 60-120 次 touch
事件,一般鼠标每秒会触发 100 鼠标事件,而显示器每秒只会刷新 60 次。假如每秒连续向主线程发送 120 次 touchmove
,就会触发 120 次命中测试,由于屏幕刷新次数远小于 120 次,有些命中测试就有点多余了。
所以 Chrome 会把连续触发的事件(如 wheel
, mousewheel
, mousemove
, pointermove
, touchmove
)结合起来,等到下一个 requestAnimationFrame()
调用前再把事件信息发送给主线程。
不过非连续的事件,如 keydown
, keyup
, mouseup
, mousedown
, touchstart
, touchend
都是立刻就通知主线程的。
对于一般应用程序,Chrome 合并连续触发事件的这个操作并不会影响用户体验,不过如果有需要,可以通过 event.getCoalescedEvents()
来获取被合并的事件的信息。
这一节介绍了浏览器是怎么处理用户输入的。