Chrome中网页应用是如何响应时间的
Chrome中网页应用是如何响应时间的
这个过程可以清晰地分为两个主要阶段:
-
操作系统层面 (Windows): 硬件事件被捕获并传递到正确的窗口。
-
浏览器层面 (Chrome): Chrome 接收事件,并在其复杂的架构中找到最终的目标并触发响应。
第一阶段:Windows 操作系统的工作
当你用手指按下鼠标按钮的瞬间,整个过程就开始了。
1. 硬件中断和原始输入
-
硬件中断: 鼠标控制器检测到物理开关的闭合,会向 CPU 发送一个硬件中断信号。
-
驱动程序: CPU 响应中断,调用鼠标驱动程序的中断处理程序。驱动程序将硬件信号转换为标准的输入数据包,其中包含关键信息:
-
dx
和dy
(坐标变化量,如果是移动) -
哪个按钮被按下(左键、右键等)
-
当前鼠标的绝对坐标 (x, y)
-
滚轮信息
-
2. 系统内核处理:win32k.sys
-
输入数据包被传递到 Windows 内核的图形子系统(
win32k.sys
)。 -
内核根据绝对的 (x, y) 坐标,计算出这个坐标点位于哪个屏幕、屏幕上的哪个位置。
3. 寻找目标窗口:窗口管理器与「命中测试」
这是关键的一步:Windows 如何知道光标下面是什么?
-
窗口管理器维护着一个所有窗口的层级列表(Z-Order),即哪个窗口在上面,哪个在下面。
-
它收到坐标 (x, y) 后,会从最顶层的窗口开始,依次向下询问:“这个点在你的客户区内吗?”
-
命中测试: 每个窗口都有一个关联的窗口过程(Window Proc)。窗口管理器会向这个窗口过程发送一个
WM_NCHITTEST
消息,并附上坐标。窗口过程的默认处理会返回一个值,告诉系统这个点是在标题栏、边框、客户区、最小化按钮还是其他部分。-
如果返回
HTCLIENT
,表示点在客户区内。 -
如果返回
HTCAPTION
,表示点在标题栏。 -
如果返回
HTMINBUTTON
,表示点在最小化按钮。 -
...等等。
-
-
这个过程会一直持续,直到找到一个返回非
HTTRANSPARENT
值的最顶层窗口。这个窗口就是被点击的窗口。
重要提示:对于 Chrome 浏览器的主窗口,你点击的“按钮”并不是一个真正的 Windows 按钮控件。Chrome 是自绘的。整个浏览器界面(标签栏、地址栏、按钮)都是 Chrome 在一个巨大的客户区内绘制出来的。所以对于 Windows 来说,它只知道你点击了 Chrome_WidgetWin_1
这个窗口的客户区(HTCLIENT
),它完全不知道里面有一个“刷新按钮”。识别这个“按钮”是 Chrome 自己的任务。
4. 输入消息的派发和排队
-
一旦找到目标窗口,系统内核会将原始的输入数据包转换成一个标准的 Windows 消息,例如
WM_LBUTTONDOWN
。这个消息包含了所有相关信息:坐标(转换为相对于目标窗口客户区的坐标)、按键状态、时间戳等。 -
这个消息被放到系统消息队列中。
-
系统的 消息派发器 会查看这个消息的目标窗口属于哪个线程。每个创建窗口的应用程序线程都有自己的线程消息队列。
-
派发器将
WM_LBUTTONDOWN
消息从系统队列移动到属于 Chrome 浏览器进程的主线程的消息队列中。
第二阶段:Chrome 浏览器的工作
现在,事件已经成功传递到了 Chrome 的主线程消息队列。Chrome 的多进程架构开始发挥作用。
5. Chrome 的主线程消息循环
-
Chrome 的主线程运行着一个经典的 Windows 消息循环(
Message Loop
)。它不断地从线程消息队列中取出消息(如WM_LBUTTONDOWN
,WM_MOUSEMOVE
,WM_PAINT
等)。 -
对于输入消息,Chrome 不会直接用
Window Proc
处理它们,而是会将这些 Windows 消息转换成一个平台无关的、Chrome 内部定义的WebInputEvent
。-
WM_LBUTTONDOWN
->blink::WebMouseEvent::ButtonDown
(类型为MouseDown
) -
坐标也会被转换为相对于渲染视图的坐标。
-
-
这个
WebInputEvent
被放入一个队列,准备发送给渲染进程。
6. 跨进程通信 (IPC)
-
Chrome 采用多进程架构。浏览器进程(Browser Process)负责管理窗口、标签页和网络等,而渲染进程(Renderer Process) 负责解析 HTML、CSS,执行 JavaScript 和排版布局(由 Blink 引擎负责)。
-
浏览器进程通过 IPC(进程间通信) 通道(通常是命名管道)将封装好的
WebInputEvent
发送给负责当前标签页的渲染进程。
7. 渲染进程接收事件:Blink 引擎的「命中测试」
渲染进程接收到 WebInputEvent
后,真正的“找按钮”过程开始了。
-
事件接收: 渲染进程的 Main Thread 接收到这个事件。
-
命中测试(Hit Test): Blink 引擎使用渲染树(Render Tree)进行命中测试。它会从最底层的节点开始(根据 CSS 的层叠上下文和 z-index),遍历到最顶层的节点,计算鼠标坐标点位于哪个渲染对象(RenderObject) 上。
-
渲染树是布局后的结果,它精确地知道每个元素(包括那个按钮)在页面上的位置和大小。
-
命中测试的结果是一个
HitTestResult
对象,里面包含了最顶层的 DOM 节点(Element
)、甚至可能是点击到的Text
节点。
-
8. 构建事件传播路径(Event Path)
-
Blink 根据
HitTestResult
找到的目标元素(比如<div id="myButton">
),开始构建事件的传播路径。 -
这个路径是一个链表,从目标元素开始,依次向上经过所有父级节点,直到
document
对象和window
对象。-
#myButton
-><body>
-><html>
->document
->window
-
9. 事件的分发和传播
-
捕获阶段(Capturing Phase): 事件从
window
对象开始,沿着传播路径向下传递到目标元素。在这个阶段,注册了捕获监听器(addEventListener(..., true)
)的父级元素会得到触发。 -
目标阶段(Target Phase): 事件到达目标元素本身。所有在目标元素上注册的监听器(无论捕获还是冒泡)都会按注册顺序执行。
-
冒泡阶段(Bubbling Phase): 事件从目标元素开始,沿着传播路径向上传递回
window
对象。注册了冒泡监听器(addEventListener(..., false)
或默认)的父级元素会得到触发。
10. 触发默认行为
-
如果事件没有被
preventDefault()
阻止,并且目标是像<input type="button">
或<button>
这样的原生按钮元素,那么在这个元素上会触发一个模拟的点击事件作为其默认行为。 -
如果是自定义的
div
按钮,则没有内置的默认行为。
11. 更新和渲染(如果需要)
-
事件处理程序可能会改变 DOM 或样式(例如,改变按钮的背景颜色表示被按下)。
-
这会触发重排(Reflow) 或重绘(Repaint)。
-
渲染进程计算新的样式和布局,生成新的绘制指令,并通过 IPC 将这些指令传回给浏览器进程。
-
浏览器进程的 GPU 进程 或UI 线程最终将这些指令提交给显卡驱动,驱动硬件在屏幕上绘制出新的像素,让你看到按钮被按下的视觉效果。
总结与流程图
事件转换过程:硬件信号
-> WM_LBUTTONDOWN
(Windows 消息) -> blink::WebMouseEvent
(Chrome 内部事件) -> DOM MouseEvent
(JavaScript 事件对象)
识别过程的对比:
-
Windows: 只识别到窗口级别(“Chrome 的客户区被点了”),使用
WM_NCHITTEST
。 -
Chrome: 识别到DOM 元素级别(“客户区里的这个按钮被点了”),使用 Blink 的渲染树进行命中测试。
事件从 DOM 层传递到目标元素的详细过程
以下是事件从 DOM 层(更准确地说,是从 Blink 引擎接收 IPC 事件开始)传递到目标元素的详细过程:
核心阶段:命中测试 (Hit Test) 与 事件传播 (Event Propagation)
整个过程可以分为两大步:
-
命中测试 (Hit Test):找到事件发生的“最具体”的目标对象。
-
构建传播路径与分发:根据找到的目标,构建一条向上传播的路径,并依次分派事件。
第一步:命中测试 (Hit Test) - “找到目标”
当渲染进程的主线程从 IPC 接收到一个 WebMouseEvent
(如 mouseDown
)时,它包含了鼠标在整个视图端口(Viewport) 中的坐标。
-
从渲染树开始:Blink 引擎不会去遍历 DOM 树来做命中测试,因为 DOM 树不包含布局和几何信息。取而代之的是,它使用渲染树(Render Tree)。渲染树由
RenderObject
组成,每个RenderObject
都知道对应 DOM 元素的准确位置、大小、裁剪区域和样式(例如pointer-events: none;
会在这里被考虑)。 -
遍历渲染树:Blink 从渲染树的根节点(
RenderView
)开始,根据stacking context
(层叠上下文) 和z-index
,以从后到前(从最底层到最顶层)的顺序遍历RenderObject
。 -
检查边界框:对于每个
RenderObject
,引擎检查事件的 (x, y) 坐标是否落在其布局边界框(Layout Border Box) 内。如果不在,则跳过其整个子树。 -
检查更细节的形状:如果坐标在边界框内,引擎会进一步检查更精确的形状:
-
对于简单矩形,检查就完成了。
-
对于复杂形状(如
clip-path
,border-radius
),会使用更精细的算法来检查点是否在可见区域内。 -
如果元素设置了
pointer-events: none;
,它会被直接跳过,事件会“穿透”它,落到下面的元素上。
-
-
找到最终目标:这个遍历过程会一直持续,直到找到位于最顶层(z-index最高) 且处于鼠标坐标之下的
RenderObject
。这个RenderObject
就是命中测试的“结果”。 -
映射到 DOM 节点:每个
RenderObject
都有一个指向其对应Node
(DOM 节点)的指针。Blink 通过这个指针,将渲染层的命中测试结果映射回 DOM 层的元素。现在我们得到了事件的目标Element
。
举个例子:你点击了一个按钮。这个按钮是一个 <div>
,它内部有一个 <span>
图标。命中测试可能会先碰到 span
的 RenderObject
,但因为 span
是 div
的子元素,并且都在同一个 stacking context 中,span
的渲染对象位于更上层。所以最终目标就是这个 span
元素。但通常,事件处理是委托给父级 div
的,这就是下一步“冒泡”要解决的问题了。
第二步:事件传播 (Event Propagation) - “沿着路径传递”
一旦找到了目标 Element
,Blink 就知道事件应该从哪里开始传播了。
-
构建事件传播路径 (Event Path):
-
Blink 以目标
Element
为起点,向上遍历其父节点,依次将每个祖先节点、document
对象、直到window
对象,添加到一个有序的链表或数组中。这个有序的节点列表就是事件的传播路径。 -
这个路径在事件被分派之前就已经构建完成,并且在整个分派过程中保持不变。这保证了事件监听器的执行不会因为动态添加/删除节点而变得混乱。
-
-
分派事件:捕获阶段 (Capture Phase):
-
引擎从传播路径的最顶端(
window
对象)开始,向下遍历到目标节点之前的最后一个节点。 -
对于路径上的每一个节点,如果该节点上使用
addEventListener(type, listener, true)
注册了捕获阶段的事件监听器,那么这些监听器就会被同步地执行。 -
event.target
始终指向最初找到的那个目标元素,而event.currentTarget
则指向当前正在处理事件的节点。
-
-
分派事件:目标阶段 (Target Phase):
-
引擎到达传播路径中的目标节点。
-
所有在目标节点上注册的监听器,无论它们是在捕获阶段注册还是冒泡阶段注册的,都会按照代码注册的顺序被执行。
-
注意:在目标节点上,捕获和冒泡监听器的区分消失了,它们都被视为目标阶段的监听器。
-
-
分派事件:冒泡阶段 (Bubble Phase):
-
引擎从目标节点的父节点开始,向上回溯到传播路径的顶端(
window
)。 -
对于路径上的每一个节点,如果该节点上使用
addEventListener(type, listener, false)
或省略 useCapture 参数(默认为false
)注册了冒泡阶段的事件监听器,那么这些监听器就会被执行。 -
有些事件(如
focus
,blur
,load
等)本身不冒泡,所以它们没有冒泡阶段。
-
-
默认行为 (Default Action):
-
在事件分发的任何阶段,都可以通过调用
event.preventDefault()
来阻止浏览器的默认行为。 -
默认行为是在整个事件分发之后才执行的。例如,点击一个链接 (
<a>
),所有的事件监听器都执行完后,如果没有人阻止默认行为,浏览器才会执行跳转。
-