七、不要使用 DOM

在这一章中,我们将回顾 DOM 与编写高性能 JavaScript 的关系,并看看如何优化 JavaScript 以使我们的 web 应用明显更快。

我们还将看看 JavaScript 动画,并测试他们的性能与现代 CSS3 动画; 我们还将测试 DOM 中的 paint redraw 事件,并快速测试附加到页面上可能影响性能的滚动事件。

在本章中,我们将讨论以下主题:

  • 为什么要担心 DOM 呢?
  • 我们不需要一个 mv 库吗?
  • 使用createElement函数创建新对象
  • 动画元素
  • 了解漆事件
  • 讨厌的鼠标滚动事件

为什么要担心 DOM?

文档对象模型(DOM)是我们的 HTML 内容在 web 浏览器中显示的方式。 它不是完全的相同的源代码; DOM 是我们在 web 浏览器中对 web 应用页面进行更新时源代码的实时更新版本。

我们可以说,快速、优化的 JavaScript 肯定会帮助我们的应用运行和性能更好,正如我们在前几章学到的。 但重要的是要理解 DOM 对 JavaScript 性能的重要性,就像理解如何优化for循环。

在 Web 的早期,我们作为 Web 开发人员并没有过多地考虑 DOM。 如果我们思考一下 JavaScript 已经走了多远,我们就会发现 web 开发世界已经发生了许多变化。 如果我们回想一下 google 之前的网络,我们知道网站是非常简单的,用户交互主要局限于超链接标签和偶尔的 JavaScriptwindow.alert()功能,以显示某种形式的应用交互。

随着时间的推移,我们遇到了 Web 2.0,或者更确切地说,在这里异步 JavaScript 和 XML(AJAX)应运而生。 如果你不熟悉 AJAX,我想总结一下:AJAX web 应用允许开发人员从外部来源获取内容,通常是 XML 文件(这是 AJAX 中的 X)。

有了 AJAX,网站内容突然变得动态了,这意味着开发者不再需要依靠后端技术来更新网页。 突然间,对更强大的 JavaScript 的需求出现了。 企业和他们的客户不再想要一个网站和页面响应闪光(或网站后端技术来更新页面使用POST提交方法),更因此与谷歌等网站地图和 Gmail 似乎把网络作为一个平台软件的概念而不是一个桌面操作系统。

我们不需要一个 mv 库吗?

今天,我们有一些框架可以帮助解决这类应用的繁重工作; AngularJS、Backbone.js、Knockout.js 和 jQuery 都是一些会让人想到的库。

然而,在本书中,我们将坚持使用普通 JavaScript,原因有二。 第一个原因是,整本书都是关于这些库的,讨论性能和不同层次的体验,所有这些都很好,但超出了本书的范围。 第二个原因是大多数开发人员通常不需要这些库来构建项目。

请记住,这里提到的所有 JavaScript 库,以及在 Web 上找到的那些库,同样都是 JavaScript! 对于大多数项目,我们不应该需要一个库来按照我们想要的方式构建项目; 此外,这些库中有许多都附带了额外的代码。

我这么说的意思是,这些库附带的特性在给定的项目中可能是不需要的,除非一个库是模块化的,否则在不删除不需要的特性的情况下使用它是很困难的。 如果您工作在一个团队环境中,其他人可能会为应用的某些领域使用共享库,这些领域可能会使用一些特性,但不是所有特性,那么这就更加困难了。

我们将在第 9 章优化 iOS 混合应用中探讨移动 JavaScript 性能。 我们会发现这些库将成为更大的负担。 现在,让我们看看破坏 DOM 的一些常见方法,以及我们可以做些什么来提高它的性能。

使用 createElement 函数创建新对象

在这里,我们将学习使用createElement函数创建新对象,以及以下三个主题:

  • 围绕createElement功能工作
  • 使用createElement功能
  • 何时使用createElement功能

围绕 createElement 函数工作

在 JavaScript 中,我们可以使用document.createElement()函数创建新的页面元素,并使用document.createTextNode()函数将文本对象放置到生成的元素中。 通常情况下,如果使用多个生成的元素创建新元素并将其注入 DOM,则会消耗渲染资源和交互性能。

使用 createElement 函数

让我们测试一下createElement函数在屏幕上呈现内容的效果如何。 下面是我们的测试:我们将使用for循环创建一个包含大量数据的表。 我们将用一个带有for循环迭代次数的文本对象填充一个表格单元格。 然后,我们将查看另一个版本,使用不同的代码实现创建相同的效果,并对两者进行比较。 让我们看看使用createElement函数的第一个选项,如下所示:

Working with the createElement function

这里,我们有一个简单的 HTML5 页面,在head部分有一些格式化的 CSS 样式,在第 21 行有一个空的占位符div元素,id设置为datainsert。 在第 25 行,我们有一个anonymous function,只要它被加载到浏览器中就可以运行; 同样在第 26 行,我们开始了一个console.time函数来计算我们的 JavaScript 执行了多长时间。 然后在第 27 行创建一个名为tableElem的表元素变量; 在第 28 行到第 31 行,我们设置了一些属性来帮助样式化表的格式。

然后在第 33 行,我们开始我们的for循环; 在我们的for循环范围,我们创建一个表行元素,一个表格单元元素,和一个文本节点插入文本生成的表格单元,从cellContent变量在 35 行,在第 36 行tableTr变量,tableTd变量在 37 行。 在第 39-41 行,我们将生成的单元格附加到表中,并继续循环10000次。 最后,我们将 table 元素添加到页面上的datainsertdiv 元素以呈现内容。 让我们在浏览器中运行这个,看看使用 Chrome开发工具选项渲染内容需要多长时间。

Working with the createElement function

正如我们所看到的,这需要相当长的处理时间,在 Chrome 中大约 140 毫秒,这是一个相当长的渲染时间。 您可以考虑在构建消息传递客户机或从 JSON 显示数据时进行类似操作。 无论如何,使用createElement功能的成本是相当大的,应该只在一小部分使用。

在不使用createElement函数的情况下,在这样的表上生成数据的另一种方法是使用innerHTML属性。 这个属性提供了一种简单的方法来完全替换元素的内容,并以与给变量赋值相同的方式赋值。 当使用innerHTML属性时,无需刷新页面即可更改页面内容。 这可以让你的网站感觉更快,对用户输入的响应更迅速。 也可以使用+=append 运算符附加该属性。 知道了这一点,我们可以以稍微不同的方式构建代码库。 我们正在做的事情如下面的截图所示:

Working with the createElement function

这个函数的布局应该与我们的createElement函数示例非常相似。 在第 21 行,我们有相同的datainsertdiv; 在第 25 行,我们的Anonymous function开始了。 现在在第 28 行,我们看到一些完全不同的东西; 在这里,我们可以看到一个名为tableContents的字符串变量的开始,它的 HTML 表的开始具有与前面示例设置的相同的属性。 这就像我们使用createElement函数所做的一样,只不过这次我们使用的是 HTML 标记的 JavaScript 字符串,而不是 DOM 对象。

在接下来的第 30 行,我们开始我们的for循环,并将tableContents字符串添加到我们的表行和表单元格中,并将 for 循环的迭代计数插入到单元格中,再次计数 10,000 次。

当循环在第 35 行结束时,我们在字符串后面加上表的右括号。 最后,在第 37 和 38 行,我们使用innerHTML属性,并将表写入到datainsertdiv 元素的innerHTML属性中。 让我们在浏览器中运行这个示例,看看它的处理时间。

Working with the createElement function

这一次,我们的表的渲染时间大约是 40 毫秒,这几乎是使用createElement函数的 4 倍。 现在这是一个很大的速度改进! 它在 Chrome 上的视觉速度甚至更快。

何时使用 createElement 函数?

虽然函数很慢,但有时它在通过复杂布局生成 HTML 时更有帮助,因为复杂的应用生成的元素比innerHTML属性的样式要多得多。

如果是这种情况,那么这样做更多的是为了方便开发团队在修改元素类型时使用,而不是为了更新完整的字符串以满足应用的需要。 在任何情况下,如果你需要创建 HTML 元素,innerHTML属性总是更快。

动画元素

其中一个令人印象深刻的 JavaScript 使用出现在 JavaScript 的Web 2.0时代,而 AJAX 正在流行; 另一个有趣的想法是 JavaScript 动画。 这些动画是通过简单地遍历一个元素的样式来创建的,这些样式是使用setInterval函数在左边和顶部放置的,然后在元素到达它的端点后解散它。 这允许 div 在页面上以补间或动画的形式出现。

用老式的方式制作动画

大多数 JavaScript 开发人员都熟悉使用 jQuery 做动画,jQuery 是流行的 JavaScript DOM 操作库,使用animate函数来创建 DOM 动画。 但是,由于我们在本书中讨论的是纯 JavaScript,让我们看一个如何从头开始构建它的示例。 查看以下截图中的代码:

Animating the old-fashioned way

在这个例子中,我只用 JavaScript 简单地创建了一个 webkit 友好的动画(这意味着它只会在谷歌 Chrome 和苹果 Safari 浏览器中正确显示)。 在第 7 行中,我们设置了一些基本样式,包括将id设置为dot的黑点 div 元素。

现在在第 27 和 28 行,我们分别声明了doti变量。 然后,在第 31 行,我们创建了一个名为interval的变量,它实际上是传递给setInterval函数的参数。 在这段代码中,它是针对每一毫秒的,如第 38 行所示。 在setInterval函数中,我们将i变量的计数加1,并更新dot元素的位置。 最后,当i变量的值与450严格相等时,我们使用clearInterval函数消去我们的区间变量,从而停止setInterval函数的进一步处理。 如果我们看看这个,我们可以在浏览器中用纯 JavaScript 看到一个简单的动画补间。 如下截图所示:

Animating the old-fashioned way

现在,您可能认为以这种方式创建setInterval函数可能会引起关注,您可能是对的。 幸运的是,作为开发者,我们现在有了为 HTML5 应用创建这样的动画的替代方法!

动画使用 CSS3

让我们重新构建这个示例使用 CSS3 和 JavaScript 只触发动画。 同样,为了简单起见,我们将简单地为以 webkit 为重点的浏览器设计样式。 让我们看看更新后的代码示例,如下面的截图所示:

Animating using CSS3

通过这个例子,我们可以看到我们的 JavaScript 有更少的行,这是一件好事; 它保持我们的内容样式完全基于 css,而不是使用 JavaScript 逻辑样式内容。

现在,在 JavaScript 方面,我们可以看到我们正在使用相同的Anonymous function在第 39 行,除了我们设置了一个超时来触发dot元素,以添加一个激活的类属性来触发 CSS3 中的动画。 在我们的示例中,第 19 到 30 行显示了这一点

不公平的绩效优势

在本书的许多代码示例中,我使用了console.timeconsole.timeEnd来检查性能,这个示例也不例外。 你可能已经注意到,我已经将每个动画示例封装在一个timetimeEnd函数中,以测量处理时间。

An unfair performance advantage

正如我们在前面的截图中所看到的,JavaScript 处理时间大约是 1900 毫秒,而 CSS3 动画大约是 0.03 毫秒。 现在,在我们得出 CSS3 方法更好的结论之前,我们必须记住,我们只使用 CSS3 来呈现页面,而 JavaScript 只处理动画的触发器。 它仍然更有效率,但应该注意的是 JavaScript 处理的代码更少。

现在对于较新的浏览器,这是构建内容动画的推荐方法,考虑到到目前为止所看到的性能改进,无论是否由 JavaScript 完成。 然而,有些项目需要旧的浏览器支持,这些项目可能无法访问 CSS3 过渡和动画,或者我们在升级应用的一部分动画的同时仍然保持兼容性。 这里有一种方法来做到这一点,同时使用相同的 javascript 动画,就像之前一样:

An unfair performance advantage

在这里,我们修改了最初的 JavaScript 示例,更新了dot元素的位置; 然而,我们在第 17 行和第 18 行添加了两行 CSS。 第一个是-webkit-transformtranslate3d属性,它只设置元素不改变位置; 在较旧的浏览器或非 webkit 聚焦的浏览器中,此属性将被忽略。 但在这里,它只是简单地将元素的位置设置为初始位置,这听起来很愚蠢,从某种程度上说,确实如此!

这实际上是告诉 DOM 运行时,它需要作为一个独特的图形进程运行; 它还告诉浏览器设备上的图形处理单元(GPU)快速绘制这个元素! will-change也是如此,这是一个类似的属性不一样的translate3d属性除了这不是更新位置只是告诉 GPU 重绘这个元素在非常高的速度和期待它在 DOM 改变。 现在,这个实践叫做向复合层添加元素。 我们将在第 9 章优化 iOS 混合应用中深入了解复合层。 但现在,这就是我们要做的; 这样,更新的浏览器仍然可以使用遗留的 JavaScript 动画获得一些视觉速度的提高。

理解油漆事件

Paint 事件是 DOM 事件,当 DOM 用 JavaScript 更新时,浏览器会绘制网页。 对于内存较低的浏览器,这可能是个问题,因为绘制事件需要大量的处理和图形渲染来显示大量的更新。

如何检查油漆事件?

通常,paint 事件可以在 Web Inspector 的时间轴视图中找到。 由于在网页浏览器中,绘图事件是按时间顺序显示的,所以在 Chrome 的开发者工具选项中,它们的显示略有不同。

打开 Chrome 的开发工具选项,点击抽屉图标(它是在开发工具选项右上方齿轮图标旁边的图标)。 打开抽屉中的Rendering选项卡,点击Show paint rectangles选项。 完成之后,刷新页面。 当页面加载时,我们将看到页面在各个区域中以绿色高亮显示。 这些是在屏幕上加载时的绘制事件。 这是一个使用我们的动画和显示在 Chrome 的开发工具选项的绘制矩形的例子:

How to check for paint events?

注意在页面加载和动画结束时,绿色的方块是如何出现的。 这是因为 DOM 只在页面加载或动画结束时重新绘制浏览器窗口。

有时候,项目可以单独使用 JavaScript 创建相当复杂的动画。 为了发现错误与我们的 JavaScript 逻辑,并确保油漆事件不会造成一个问题,我们可以使用 Chrome 的开发者工具中的连续页面重绘功能。

测试涂料事件

为了测试这一点,我们已经设置了一个带有内置 bug 的 JavaScript 动画,如下所示:

Testing paint events

这看起来应该是非常类似于我们在本章中创建的早期动画。 但是如果我们看一下第 35 行到第 38 行,我们可以看到有一个条件语句else if,用来检查增量变量i是否在 250-258 范围内; 如果是,则将left``styledot元素中移除。

当我们运行这个,我们应该遇到一个闪烁动画到达这一点。 我们可以验证这是否真的是一个 JavaScript 问题,通过启用 Chrome 的开发者工具的连续页面重绘。

要做到这一点,打开开发人员工具选项,打开抽屉,并点击抽屉中的渲染选项卡。 然后我们可以检查Enable 连续页面重绘Show paint rectangles选项。 当我们这样做时,我们的网页应该显示一个绿色覆盖层,并在浏览器窗口的右上角显示一个信息框。 如下截图所示:

Testing paint events

现在,当我们重新加载页面并重新播放动画时,我们的dot元素应该在整个动画期间显示一个围绕它的绿色框。 这是 Chrome 迫使页面不断重绘,因为动画更新。 正如我们所看到的,即使我们遇到了预先设定的错误,这个框仍然是正确的,这表明了一个 JavaScript 问题。 如果这是一个真正的油漆问题,当重绘问题发生时,该框将消失。

讨厌的鼠标滚动事件

当你使用 JavaScript 工作时,油漆事件(或缺乏)不是唯一的问题。 应用于浏览器窗口或文档的滚动事件可能会对应用造成严重破坏; 通过滚动鼠标来连续触发事件从来都不是一个好主意,更不用说多个事件了。

如果我们正在编写一个应用,我们知道应用是否添加了一个或多个事件。 但如果我们要更新一个 web 应用,在 Chrome 的开发者工具中有一个工具,可以让我们直观地检查滚动事件。

让我们创建一个快速示例来说明这个特性是如何工作的,以及在尝试优化 DOM 接口时它需要什么。 为此,我创建了一个mousewheel事件捕获XY 坐标的鼠标指针的位置的页面,并打印到一个输入字段与一组id``txtfield; 每次移动鼠标滚轮都会触发。 让我们看看下面的代码示例:

Pesky mouse scrolling events

我们可以在这里看到页面本身很轻,但是在第 23 行,我们可以看到mousewheel事件监听器正在使用第 26 行getMouseLocation函数添加连续事件。 然后,在第 27 行,我们的输入字段(带有txtfieldid)被分配了一个带有鼠标事件信息的字符串,抓取鼠标指针的XY坐标,并将其应用于txtfield的值。 现在让我们看看开发工具突出显示滚动的性能问题。

打开抽屉,打开Rendering选项卡,然后点击Show potential scroll 瓶颈。 这将突出显示 JavaScript 中分配了滚动事件的块区域; 下面是启用过滤器后的示例:

Pesky mouse scrolling events

现在,就性能而言,这本身并不是太糟糕,但具有多个鼠标移动事件的应用可能会导致问题,如果移动区域重叠的话就更严重了。 如果我们向文本区域添加相同的事件监听器,并从文档中删除该监听器,我们会在开发人员工具过滤器中看到多个滚动监听器实例吗? 让我们通过查看本章的最后一个示例文件07_08.html的输出来找出它:

Pesky mouse scrolling events

不! 正如我们所看到的,即使在单个元素上启用了mousewheel事件,整个页面也会高亮显示。 由于可以在 DOM 顶部检查mousewheel事件,因此即使应用只关注mousewheel事件的一个小元素,整个页面也会受到影响。

因此,记住事件很重要,因为它们可能会降低页面的性能。

小结

在这一章中,我们讨论了 JavaScript 如何影响 DOM 的性能; 我们回顾了createElement函数,学习了如何更好地编写 JavaScript 来优化从代码中生成元素。

我们还回顾了 JavaScript 动画,并比较了它们与现代 CSS3 动画的性能。 我们还学习了如何优化现有或遗留的 JavaScript 动画。

最后,我们回顾了 DOM 中的 paint 事件,并看到了在 JavaScript 操作之后 DOM 是如何重新绘制内容的; 我们还讨论了mousewheel事件,并了解了它们如何潜在地降低 DOM 的速度。

在下一章,我们将看看 JavaScript 在性能方面的新朋友:web workers,以及我们如何让 JavaScript 像多线程应用一样运行。