Chrome中网页应用是如何响应时间的

Chrome中网页应用是如何响应时间的

 

这个过程可以清晰地分为两个主要阶段:

  1. 操作系统层面 (Windows): 硬件事件被捕获并传递到正确的窗口。

  2. 浏览器层面 (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_LBUTTONDOWNWM_MOUSEMOVEWM_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)

整个过程可以分为两大步:

        1. 命中测试 (Hit Test):找到事件发生的“最具体”的目标对象。

        2. 构建传播路径与分发:根据找到的目标,构建一条向上传播的路径,并依次分派事件。


第一步:命中测试 (Hit Test) - “找到目标”

当渲染进程的主线程从 IPC 接收到一个 WebMouseEvent(如 mouseDown)时,它包含了鼠标在整个视图端口(Viewport) 中的坐标。

        1. 从渲染树开始:Blink 引擎不会去遍历 DOM 树来做命中测试,因为 DOM 树不包含布局和几何信息。取而代之的是,它使用渲染树(Render Tree)。渲染树由 RenderObject 组成,每个 RenderObject 都知道对应 DOM 元素的准确位置、大小、裁剪区域和样式(例如 pointer-events: none; 会在这里被考虑)。

        2. 遍历渲染树:Blink 从渲染树的根节点(RenderView)开始,根据 stacking context(层叠上下文) 和 z-index,以从后到前(从最底层到最顶层)的顺序遍历 RenderObject

        3. 检查边界框:对于每个 RenderObject,引擎检查事件的 (x, y) 坐标是否落在其布局边界框(Layout Border Box) 内。如果不在,则跳过其整个子树。

        4. 检查更细节的形状:如果坐标在边界框内,引擎会进一步检查更精确的形状:

          • 对于简单矩形,检查就完成了。

          • 对于复杂形状(如 clip-pathborder-radius),会使用更精细的算法来检查点是否在可见区域内。

          • 如果元素设置了 pointer-events: none;,它会被直接跳过,事件会“穿透”它,落到下面的元素上。

        5. 找到最终目标:这个遍历过程会一直持续,直到找到位于最顶层(z-index最高) 且处于鼠标坐标之下的 RenderObject。这个 RenderObject 就是命中测试的“结果”。

        6. 映射到 DOM 节点:每个 RenderObject 都有一个指向其对应 Node(DOM 节点)的指针。Blink 通过这个指针,将渲染层的命中测试结果映射回 DOM 层的元素。现在我们得到了事件的目标 Element

举个例子:你点击了一个按钮。这个按钮是一个 <div>,它内部有一个 <span> 图标。命中测试可能会先碰到 span 的 RenderObject,但因为 span 是 div 的子元素,并且都在同一个 stacking context 中,span 的渲染对象位于更上层。所以最终目标就是这个 span 元素。但通常,事件处理是委托给父级 div 的,这就是下一步“冒泡”要解决的问题了。

第二步:事件传播 (Event Propagation) - “沿着路径传递”

一旦找到了目标 Element,Blink 就知道事件应该从哪里开始传播了。

        1. 构建事件传播路径 (Event Path):

          • Blink 以目标 Element 为起点,向上遍历其父节点,依次将每个祖先节点、document 对象、直到 window 对象,添加到一个有序的链表或数组中。这个有序的节点列表就是事件的传播路径。

          • 这个路径在事件被分派之前就已经构建完成,并且在整个分派过程中保持不变。这保证了事件监听器的执行不会因为动态添加/删除节点而变得混乱。

        2. 分派事件:捕获阶段 (Capture Phase):

          • 引擎从传播路径的最顶端(window 对象)开始,向下遍历到目标节点之前的最后一个节点。

          • 对于路径上的每一个节点,如果该节点上使用 addEventListener(type, listener, true) 注册了捕获阶段的事件监听器,那么这些监听器就会被同步地执行。

          • event.target 始终指向最初找到的那个目标元素,而 event.currentTarget 则指向当前正在处理事件的节点。

        3. 分派事件:目标阶段 (Target Phase):

          • 引擎到达传播路径中的目标节点。

          • 所有在目标节点上注册的监听器,无论它们是在捕获阶段注册还是冒泡阶段注册的,都会按照代码注册的顺序被执行。

          • 注意:在目标节点上,捕获和冒泡监听器的区分消失了,它们都被视为目标阶段的监听器。

        4. 分派事件:冒泡阶段 (Bubble Phase):

          • 引擎从目标节点的父节点开始,向上回溯到传播路径的顶端(window)。

          • 对于路径上的每一个节点,如果该节点上使用 addEventListener(type, listener, false) 或省略 useCapture 参数(默认为 false)注册了冒泡阶段的事件监听器,那么这些监听器就会被执行。

          • 有些事件(如 focusblurload 等)本身不冒泡,所以它们没有冒泡阶段。

        5. 默认行为 (Default Action):

          • 在事件分发的任何阶段,都可以通过调用 event.preventDefault() 来阻止浏览器的默认行为。

          • 默认行为是在整个事件分发之后才执行的。例如,点击一个链接 (<a>),所有的事件监听器都执行完后,如果没有人阻止默认行为,浏览器才会执行跳转。

 

WebDriver 点击的核心流程(与真实点击的差异)

如上图所示,WebDriver 跳过了操作系统层面的漫长旅程,其核心流程如下:

  1. 协议连接:你的测试脚本(Python, Java等)通过 WebDriver协议 (也称为 JSON Wire Protocol)与浏览器驱动(如ChromeDriver)通信。驱动则通过 DevTools Protocol 与浏览器实例建立连接。

  2. 元素解析与坐标计算:

    • 当你调用 element.click() 时,WebDriver 首先会确保目标元素存在、可见且可操作(会检查CSS属性如 displayvisibilitypointer-events 等)。

    • 它同样需要计算元素的坐标。但它不是通过渲染树命中测试,而是通过执行JavaScript指令来获取元素的几何信息(例如,调用 element.getBoundingClientRect())。

  3. 事件注入(最关键的区别):

    • 它不模拟硬件输入。WebDriver 不会要求操作系统模拟一个 WM_LBUTTONDOWN 消息。

    • 相反,它通过 DevTools Protocol 向渲染进程发送命令,要求它在指定的DOM元素上直接合成(Synthesize)并分发(Dispatch)一个事件。

    • 这意味着,事件直接在Blink引擎内部生成,形如 blink::WebMouseEvent,并直接进入上述流程图中的 “渲染进程” 阶段。

  4. 直接触发JavaScript行为:

    • 合成的事件会正常进行事件传播(捕获、目标、冒泡),触发绑定在元素上的所有 click 事件监听器。

    • 对于类似 <a> 标签的导航、表单的提交等默认行为,WebDriver 通常也会通过执行相应的JavaScript来触发(例如,对于链接,它可能会读取 href 属性然后通过 window.location 进行跳转),而不是依赖浏览器真正的默认行为。这确保了操作的可靠性。


为什么要有这种差异?优缺点是什么?

原因与优点:

  1. 可靠性:绕过OS和UI的复杂性。真实鼠标点击可能会因为元素被突然遮挡、动画、光标速度设置等问题而失败。WebDriver直接针对DOM元素操作,只要元素在DOM中且状态可用,点击就会成功。

  2. 速度:避免了硬件模拟和系统消息队列的延迟,通常更快。

  3. 跨平台一致性:无论在Windows、macOS还是Linux上运行测试,其行为都是一致的,因为它不依赖于特定操作系统的输入机制。

  4. 无外设测试:可以在没有图形界面(Headless)的服务器上运行。

缺点与潜在问题:

  1. “不像真人”:这是最大的缺点。因为绕过了OS层,一些依赖于系统事件的行为无法被触发。

    • 例如:某些网页可能会监听 mousedownmouseup 的详细属性(如 clientXscreenX),或者通过检查 event.isTrusted 属性来判断事件是来自真实用户(true)还是脚本合成的(false)。WebDriver事件 event.isTrusted 为 false,因此可以被网页检测并拒绝。

  2. 视觉反馈缺失:不会真正触发元素的 :active 等CSS伪类状态,因为你看不到浏览器界面,这通常无关紧要,但有时是验证点的一部分。

高级模式:Actions API

为了模拟更真实的行为,WebDriver提供了 Actions API (如 actions.move_to_element(element).click().perform())。

  • Actions API 会尝试模拟更精细的鼠标移动、按下和释放过程。

  • 它可能会生成一系列更逼真的中间事件(如 mousemove),并且其生成的事件对象的 isTrusted 属性可能仍然是 false,但它的行为更贴近真实用户。

  • 在底层,它仍然是通过DevTools Protocol合成事件,而不是驱动OS级别的输入。

总结

 
特性真实用户点击WebDriver click()
起点 硬件中断 → OS内核 WebDriver协议 → DevTools协议
OS参与 是,完整的消息循环和命中测试 否,完全绕过
浏览器进程 是,接收OS消息并转发 弱,仅作为协议桥梁
渲染进程 是,进行Blink命中测试和事件传播 是,但事件是直接注入的,跳过初始命中测试
事件对象 isTrusted: true isTrusted: false
可靠性 低,易受UI变化影响 高,直接锁定DOM元素
真实性 100% 真实 模拟,可能被网页检测到

 

 

 

 

 

 

 

 

 

所以,WebDriver操作是“直奔主题”的程序化点击,它保证了测试的效率和可靠性,但牺牲了完全的真实性。 而真实用户的点击则是一个完整的、自底向上的物理和系统过程。

 
posted @ 2025-09-04 11:14  卖雨伞的小男孩  阅读(9)  评论(0)    收藏  举报