写点什么

Web 键盘输入法应用开发指南(10)—— 性能与原理

作者:天择
  • 2022 年 3 月 23 日
  • 本文字数:3529 字

    阅读完需:约 12 分钟

Web 键盘输入法应用开发指南(10)—— 性能与原理

引言

在本文中,我们先来讨论事件处理时的性能问题,尤其是针对键盘和输入法事件处理流程的性能优化。然后我们稍微深入到浏览器的实现,探究一下从用户按下键盘,到输入的文本出现在页面上,中间经历了什么。

性能优化

Web 应用程序的性能因素有很多,即使只考虑前端的因素也不少,如浏览器缓存、页面渲染、JavaScript 的解析和执行等等。我们这里只关注与 UI 事件处理相关的性能点。

JavaScript 优化

首先考虑 JavaScript 本身的写法。通常来说,我们给一个输入控件添加事件处理程序(如 keydown)后,这个处理程序会在短时间内频繁被调用,比如输入一段文字。如果是与鼠标拖动和滚动相关的事件,可能会更加频繁。此时,在处理程序内部的 JavaScript 的性能问题就值得关注了。


尽管目前浏览器的渲染引擎和 JS 引擎性能都比较强大,对于复杂业务逻辑来说,注重代码的性能仍是十分有益的。这里结合《高性能 JavaScript》一书[1],给出几个建议。


  1. 标识符解析的性能

一般来说,标识符所在的位置越深,读写速度越慢。对于在循环中或者在频繁触发的事件处理器中使用的变量,最好将变量暂存为临时变量,避免多次无效读取。


const keydownHandler = (evt) => {    if (glob.environment.app.isMyApp) {        ...    }}
复制代码


这段变量访问完全可以提到事件处理之外,以减少调用。


  1. 注意作用域的影响

有一些语法特性会临时改变作用域链,而这是由性能损耗的。比如 try-catch 块,当程序发生异常进入 catch 块时,这里定义的局部变量都会加入作用域链,放在异常对象之后,异常处理。因此好的做法是使用一个独立的函数处理异常,这样作用域链中就只有头部一个对象(ex):


try {    thisWillThrowException();} catch (ex) {    HandleException(ex);}
复制代码


另外闭包也有类似的问题,比如在一个函数执行时绑定了一个事件处理函数,并访问了局部变量:


function foo() {    var id = "12";    document.getElementById("input").onkeydown = (evt) => {        handleKey(evt, id);    };}
复制代码


keydown 事件处理器访问了id变量,因此是一个闭包,它在每次foo函数调用时都会创建以便,不是一个好的做法。


  1. 减少 DOM 的修改

有时我们需要根据键盘事件来修改 DOM 元素的行为,修改行为本身就是有代价的,如果涉及了页面的重排、重绘的过程,则代价更高。因此,尽量避免直接使用 JavaScript 修改 DOM,而是改用 CSS 等方式。如果修改是必要的,也要避免多次重复操作(比如在循环或者事件处理器中)。

事件处理器数量

最好不要给过多的元素添加事件处理器。实验表明,这种做法会显著降低页面性能,尽管每个元素只绑定一个事件处理程序[2]。这还只是绑定事件的操作,没有涉及具体的事件处理程序被调用的性能。你可以从这个基于 jQuery 的实验页面找到一些数据:


添加事件处理程序的性能


通过.many样式添加的click事件处理程序会作用于大量元素,因此速度最慢。而通过父元素添加事件处理程序有着最好的性能,这就是事件委托(Event Delegation)。

事件委托

事件委托[3]就是为了解决上述问题,通过在父元素添加事件处理程序,避免给过多子元素添加。理想情况下,我们只需要给 document 文档对象添加需要的事件处理器即可:


document.addEventListener('click', function(event) {    let id = event.target.id;    if (!id) return;    let elem = document.getElementById(id);    ...});
复制代码


然后通过 event 对象的target属性来识别到底是哪个子元素触发了该事件,进行相应的处理。不过我们一般不会在document上直接使用事件委托,而是在 DOM 树的某个较大节点上使用,用于处理其子节点的事件。这样可以避免比必要的事件处理。事件委托还有一个好处是,当删除一个子元素时,我们不必要考虑删除其对应的事件处理程序,而是由父元素统一管理了。


看起来这个做法不错,不过事件委托也有一些问题。比如,它要求事件冒泡不能关闭,要一直冒泡到目标父元素上,这个程序的实现带来了潜在的限制。另外一点涉及连续触发的事件,如鼠标滚轮的wheel事件,或者触屏事件touchstart


浏览器在处理这类事件时,会涉及合成线程(Compositor)和主线程的交互。在现代浏览器架构中,页面渲染和脚本执行是有一个独立的渲染进程完成的。而渲染进程又创建了多个线程,比如处理网络和输入事件的 I/O 线程,页面渲染的主线程,以及负责合成图层的合成线程


因为事件处理程序需要在主线程中执行,因此合成线程需要在合适的时候通知主线程调用事件处理程序。合成线程会把页面上有事件处理程序的区域标记为“非快速滚动区域”(Non-Fast Scrollable Region)。如果不在这类区域中,合成线程会直接合成下一帧,而不会等待主线程,从而优化性能[4]。


非快速滚动区域


在事件委托的场景下,可能整个页面都在监听连续事件,那么整个 document 都是非快速滚动区域,那么合成线程就会不停地等待主线程的执行结果。不过此时可以给事件处理程序传递passive属性进行优化,详细可以参考文档[5]。

深入事件处理

下面我们以 Chrome 为例深入浏览器内部探究一下,从用户按下键盘到字符出现在屏幕,中间都经历了哪些过程。


以 Windows 平台为例,当键盘上某个键被按下时,首先是操作系统进行处理,并产生相应的按键事件。随后这个事件被派发(Dispatch)到当前活跃的窗体(Window)。我们的 Chrome 浏览器作为 Windows 应用程序,也是通过 Windows 窗体来实现的,那么它也可以获得这个事件。这一点可以通过 Spy++等软件确认。

Chrome Window in Spy++


在 Chrome UI 上,各个组成部分其实也是一种窗体,包括搜索栏、地址栏、页面区域等。Chrome 把这些窗体提供了一个抽象层Auro,同时 Chrome 维护了一个DesktopWindowTreeHost的窗体负责派发来自操作系统的各类事件。到达各个Auro窗体的事件,又会被发送到View层。


Chrome Window Abstraction


View 层是 Chrome 设计的平台无关的 UI 框架,它将所有的 View 组织成一个树结构。你可以将 View 理解为浏览器 UI 组件的一个抽象,事件在各层 View 间逐层传递。比如,我们熟悉的 Web 页面就在Web Contents这一层 View 中渲染。


Views in Chrome


在 Auro Window 抽象层中,消息和事件的传递需要事件派发器(Event Processor)和事件定位器(Event Targeter)共同完成。前者负责把后者找到的第一个事件,发送给目标对象处理。处理事件的可能是浏览器进程中的某个功能组件(Widget)。对于键盘事件,还可能会先调用ui::InputMethod::DispatchKeyEvent去处理,以便于与系统输入法交互完成输入


一般情况下,调用相应组件的NativeWidgetAura::OnEvent()方法处理事件。而对于页面相关的事件,会调用RenderWidgetHostViewAura::OnEvent() 处理,这里就会把事件交给 Chrome 的渲染引擎 Blink 了。有些浏览器的保留事件,比如 CTRL+T(打开新 Tab 页)的键盘事件,不会发送给 Web 页面处理。


Chrome Event Handling


事件到了 View 这一层,也有相应的事件定位器(Event Targeter),用于寻找处理事件的 View 实例。Web Contents 相关的 View 拿到键盘事件,就可以交给渲染进程处理了。


前面的章节提到过,每个渲染进程(每个 Tab 都有一个)都维护了一个 I/O 线程,它会接受和发送其他进程(如浏览器进程、网络进程)的数据。来自浏览器进程的键盘事件通过 I/O 线程到达渲染主线程,并由主线程维护的RenderViewHost::OnMessageReceived处理。接着这个事件会被转换为标准的 HTML 事件对象进入 DOM 结构,开启我们熟悉的冒泡和捕获过程,处理函数依次被加入消息队列,等待被执行。


从上面的架构不难看出,Chrome 的多层结构都有类似的特点,从 Window、Widget 到 View 和页面,在事件处理时的模型都是类似的。到了页面的 DOM 结构中,也依然按照树形进行分发和处理。


这就是一个键盘事件从被系统产生,到被浏览器中页面处理的完整流程,但只是提供了一个梗概,仅供参考。以上过程分析基于 Chrome 源码提供的相关文档[6][7],如有理解不当之处请批评指正。

总结

在本文中,我们首先探讨了在处理键盘和输入法相关逻辑时,可能遇到的性能问题及其解决方案。然后,深入浏览器内部,探索了事件派发和处理的流程。在下一篇文章中,我会对这个系列做个总结。

参考阅读

系列导航

如果您对这个系列感兴趣,可以通过下面的导航找到对应文章👇🏻。

Web 键盘输入法应用开发指南(1)— 基本概念

Web 键盘输入法应用开发指南(2)— 键盘事件

Web 键盘输入法应用开发指南(3)— 输入法事件

Web 键盘输入法应用开发指南(4)— 组合键

Web 键盘输入法应用开发指南(5)— 实战技巧

Web 键盘输入法应用开发指南(6)— 开发实战(一)

Web 键盘输入法应用开发指南(7)— 开发实战(二)

Web 键盘输入法应用开发指南(8)— 模拟事件

Web 键盘输入法应用开发指南(9)— 标准与实现

Web 键盘输入法应用开发指南(10)— 性能与原理

发布于: 刚刚阅读数: 3
用户头像

天择

关注

还未添加个人签名 2020.09.07 加入

爱看点书的软件工程师 主页:zesi.tech 欢迎交流~

评论

发布
暂无评论
Web 键盘输入法应用开发指南(10)—— 性能与原理_JavaScript_天择_InfoQ写作平台