本章内容:可维护的代码、保证代码性能、部署代码
Web 开发自 过去的 荒芜状态现在已经演化成了 完整的研究规范,并建立了种种最佳实践。随着简单的网站成长为更加复杂的Web 应用,Web 开发的世界充满了各种关于最新技术和开发的信息。尤其是 JavaScript,他从大量的研究的和推断中获益。JavaScript 的最佳实践分成若干类,并在开发过程的不同点上进行处理。
一、可维护性
编写可维护的代码很重要,因为大部分开发人员都花费大量时间维护他人代码。很难从头开发新代码,很多情况下是以他人的工作成果为基础的。确保自己代码的可维护性,以便其他开发人员在此 基础上更好的开展工作。
1.1、什么是可维护的代码
如果是 代码是可维护的,他需要遵循以下特点:
- 可理解性:其他人可以接手代码并理解它的意图和一般途径,而无需开发人员的完整解释
- 直观性:代码中的东西一看就能明白,不管其操作过程多么复杂
- 可适应性:代码以一种数据上的变化不要求完全重写的方法撰写。
- 可扩展性:在代码架构上已考虑到未来允许对核心功能进行扩展
- 可调试性:当有地方出错时,代码可以给予你足够的信息来尽可能直接地确定问题所在
1.2、代码约定
由于 JavaScript 的可适应性,代码约定对它很重要
1.2.1、可读性
要让代码可维护,首先它必须可读。可读性的大部分内容都是和代码的缩进相关的。当所有人都使用 一样的缩进方式时,整个项目的代码都会更加抑郁阅读。
通常会使用 若干空格而非制表符来进行缩进(因为制表符在不同的文本编辑器中显示效果不同)常见的缩进大小为 4 个 空格
可读性的另一方面是注释,对每个方法的注释都会视为一个可行的实践。在 JavaScript中为每个函数编写文档就更重要了。
一般而言,如下一些地方需要进行注释:
- 函数和方法—— 每个函数或方法都应该包含一个注释,描述其目的和用于完成任务所可能使用的算法。陈述事先的假设也非常重要,如参数代表什么,函数是否有返回值
- 大段代码—— 用于完成单个任务的多行代码应该在前面放一个描述任务的注释
- 复杂的算法—— 如果使用了一种独特的方式解决某个问题,则要在注释中解释你是如何做的。这不仅仅可以帮助其他浏览你代码的人,也能在下次你自己查阅代码的时候帮助理解。
- Hack—— 因为存在浏览器差异,JavaScript代码一般会包含一些 hack。不要假设其他人在看代码的时候能够理解 hack 所要应付的浏览器问题。如果因为某种浏览器无法使用 普通的方法,所以你需要用一些不同的方法,那么请将这些信息放在注释中。这样可以减少出现这种情况的可能性;有人偶然看到你的 hack,然后“修正”了它,最后重新引入了你本来修正了的错误。
1.2.2、变量和函数命名
适当给变量和函数起名字对于增加代码的可理解性和可维护性是非常重要的
命名的一般规则如下:
- 变量名应为名词 如 car 或 person。
- 函数名应该以动词开始,如 getName()。返回布尔类型值的函数以便以 is 开头,如 isEnable()
- 变量和函数都应该使用 合乎逻辑的名字,不要担心长度。长度问题可以通过后续的处理和压缩解决。
1.2.3、变量类型透明
由于 在 JavaScript 中变量是松散类型的,很容易就忘记变量所应包含的数据类型。何时的命名方式可以一定程度上缓解这个问题,但放到所有的情况下看,还不够。
有三种表示变量数据类型的方式:
- 初始化。当定义一个变量后,他应该被初始化一个值,来暗示它将来应该如何应用。
var arr = []
var str = ''
vvar num = 0
- 使用匈牙利标记法来指定变量类型(在变量名之前加上一个或多个字符)
var bFound // 布尔 Boolean
var iCount // 整数 int(Number)
var sName // 字符串 String
var oPerson // 对象 Object
这种的缺点是让代码某种程度上难以阅读,阻碍了没有用它时代码的直观性和句子式的特质。
- 使用类型注释。
var found /*: Boolean */ = false
var count /* :Number */ = 10
以上是常见的三种指定变量数据类型的方法
1.3、松散耦合
只要应用某个部分过于依赖另一部分,代码就是耦合过紧,难于维护。 因为Web 应用所涉及的技术,有很多情况会使它变得耦合过紧。必须小心这些情况,并尽可能维护弱耦合的代码
1.3.1、解耦 HTML / JavaScript
一种最常见的耦合类型是 HTML / JavaScript 耦合。在 Web 上,HTML 是数据,JavaScript 是行为。因为它们天生就需要交互,所以有很多种不同的方法将这两个技术关联起来。但是,有一些方法会将 HTML 和 JavaScript 过于紧密的耦合在一起。
``
直接在 HTML 中写 JavaScript ,使用包含内联代码的
理想情况是, HTML 和 JavaScript应该完全分离,并通过外部文件和使用 DOM 附加行为来包含 JavaScript
HTML 和 JavaScript 的紧密耦合 也可以在相反关系上成立; JavaScript 包含了 HTML。这通常会出现在 使用 innerHTML 来插入一段 HTML 文本到页面上这种情况中。
// 将 HTML 紧密耦合到 JavaScript
function insertMessage(msg) {
var container = document.getElementById('container')
container.innerHTML = '' + msg + '
'
}
一般来说,你应该避免在 JavaScript 中创建大量 HTML。对数据或者布局的更改也会要求更改 JavaScript,这也表明了这两个层次过于紧密地耦合了。
将 HTML 和 JavaScript 解耦可以在调试过程中节省事件,更加容易确定错误的来源,也减轻维护的 难度:更改行为只需要在 JavaScript 文件中进行,而更改标记则只要在渲染文件中。
1.3.2、解耦 CSS / JavaScript
JavaScript 和 CSS 也是非常紧密相关的:他们都是 HTML 之上的层次,因此常常在一起适用。
最常见的紧密耦合的例子是使用 JavaScript 来更改某些样式
// css 对 JavaScript的 紧密耦合
element.style.color = 'red'
element.style.bakcground = 'cyan'
当显示出现任何问题时都他应该只查看 CSS 文件来解决,当使用了 JavaScript 来更改某些样式的时候,如果未来需要更改样式表,CSS 和 JavaScript 文件可能都需要修改。这就给开发人员 造成了维护上的噩梦。所以在两个层次之间必须有清晰的划分。
虽然不可能完全将 CSS 和 JavaScript 解耦,但是还是能让耦合更松散的。这是通过 动态更改 样式类而非特定样式来实现的。
// CSS 对 JavaScript 的松散耦合
element.className = 'edit'
好的层次划分是非常重要的。显示问题的唯一来源应该是 css,行为问题的唯一congshijian来源应该是 JavaScript。在这些层次之间保存松散耦合 可以让你的整个应用更加易于维护。
1.3.3、解耦应用逻辑 / 事件处理程序
每个 Web 应用一般都有相当多的事件处理程序,监听着无数不同的事件。然而,很少有能仔细得将应用逻辑从事件处理程序中分离的。
function handleKeyPress(event) {
if (event.keyCode == 13) {
var target = event.target
var value = 5 * parseInt(target.value)
if (value > 10) document.getElmentById('error-msg').style.display = 'block'
}
}
较好的方法是将应用逻辑和事件处理程序相分离,这样两者分别处理各自的东西。一个事件处理程序应该从事件对象中提取相关信息
function validateValye(value) {
value = 5 * parseInt(value)
if (value > 10) document.getElmentById('error-msg').style.display = 'block'
}
function handleKeyPress(event) {
if (event.keyCode == 13) {
var target = event.target
validateValye(target.value)
}
}
handleKeyPress() 函数确认是 按下了 Enter 键。validateValue() 中没有任何东西会依赖于任何事件处理程序逻辑,它只是接收一个值,并根据改值进行其他处理。
从事件处理程序分离应用逻辑有几个好处:
- 可以让你更容易更改触发特定过程的事件
- 可以在不附加到事件的情况下测试代码,使其更易创建单元测试或是自动化应用流程
以下是要牢记的应用和业务逻辑之间松散耦合的几条原则:
- 勿将 event 对象传给其他方法;只传来自 event 对象中所需的数据
- 任何可以在应用层面的动作都应该可以在不执行任何事件处理程序的情况下进行
- 任何事件处理程序都应该处理事件,然后将处理转交给应用逻辑
1.4、编程实践
书写可维护的 JavaScript 并不仅仅是关于如何格式化代码;他还关系到代码做什么的问题。
1.4.1、尊重对象所以权
JavaScript的动态性质使得几乎任何东西在任何事件都可以修改,ECMAScript 5 中通过引入防篡改对象得以改变;不过,默认情况下所有对象都是可以修改的。这样就可以以不可预计的方式覆写默认的行为。
也在在企业环境中最重要的编程事件就是尊重对象所有权,它的意思是你不能修改不属于你的对象。
更具体的说:
- 不要为实例或原型添加方法
- 不要为实例或原型添加属性
- 不要重定义已存在的方法
最佳的方法便是永远不修改不是由你所有的对象。所谓拥有对象,就是说这个对象是你创建的,比如你自己创建的自定义类型或对象字面量。而 Array、document 这些显然不是你的,他们在你的代码执行前就存在了。
可以通过以下方式为对象创建新的功能。
- 创建包含所需功能的新对象,并用它与相关对象进行交互
- 创建自定义类型,继承需要进行修改的类型。然后可以为自定义类型添加额外的功能。
1.4.2、避免全局变量
与尊重对象所有权 密切相关的是尽可能避免全局变量和函数。这也关系到创建一个脚本执行的一致的和可维护的环境。最多创建一个全局变量,让其他对象和函数存在其中。
var MyApplication = { // 一个全局变量 —— 推荐方式
name: 'Nicholas',
sayName: function() {
alert(this.name)
}
}
单一的全局量的延伸便是命名空间的概念,命名空间有助于确保代码可以在同一个页面上与其他代码以无害的方式一起工作。
1.4.3、避免与 null 进行比较
由于 JavaScript 不做任何自动的类型检查,所以他就成了 开发人员的责任。
最常见的类型检测就是查看某个值 是否 为 null。但是,直接将值与 null 比较是使用过度的,并且常常由于不充分的类型检查导致错误。
function sortArray(values) {
if (values != null) { // 避免!
values.sort(comparator)
}
}
现实中,与 null 比较很少适合情况而被使用。函数按照下面的方式修改会更加合适
function sortArrat(values) {
if (values instanceof Array) { // 推荐
values.sort(comparator)
}
}
如果看到了与 null 比较的代码,尝试 使用 以下技术替换:
- 如果值为一个引用类型,使用 instanceof 操作符进行检查其构造函数
- 如果值为一个基本类型,使用 typeof 检查其类型
- 如果是希望对象包含某个特定的方法名,则使用 typeof 操作符确保指定名字的方法存在于对象上
1.4.4、使用常量
将数据从应用逻辑分离出来的思想可以在不冒引入错误的风险的同时,就改变数据
如下:
function validate(value) {
if (!value) {
alert('Invalid value!')
location.href = '/errors/invalid.php/'
}
}
通过将数据抽取出来变成单独定义的常量的方式,将应用逻辑与数据修改隔开来。
var Constants = {
INVALID_VALUE_MSG: 'Invalid value!',
INVALID_VALUE_URL: '/errors/invalud.php'
}
function validate(value) {
if (!value) {
alert(Constants.INALID_VALUE_MSG)
location.href = Constants.INVALID_VALUE_URL
}
}
这些设置允许数据在无需解除使用它的函数的情况下进行变更。Constants 对象甚至可以完全在单独的文件中进行定义,同时该文件可以由包含正确值的其他过程根据国际化设置来生成
关键在于将数据和使用它的逻辑进行分离。要注意的值的类型如下所示:
- 重复值——任何在多出用到的值都应抽取为一个常量。这就限制了当一个值变了而另一个没变的时候会造成的错误。这也包含了 css 类名。
- 用户界面字符串——任何用于显示个用户的字符串,都应被抽取出出来以方便国际化
- URLs—— 在 Web 应用中,资源位置很容易变更,所以推荐用一个公共地方存放所有的 URL。
- 任意可能会更改的值—— 每当你在用到字面量值的时候,你都要问一下自己这个值在未来是不是会变化。如果答案是“是”,那么这个值就应该被提取出来作为一个常量。
二、性能
JavaScript 最初是一个解释型语言,执行速度要比编译型语言慢得多。Chrome 是第一款内置优化引擎,将 JavaScript 编译成本地代码的浏览器。即使到了编译执行 JavaScript 的新阶段,任然会存在 低效率的 代码。不过,还是有一些方式可以改进代码的整体性能。
2.1、注意作用域
随着作用域链中的作用域数量的增加,访问当前作用域以外的变量的时间也在增加。访问全局变量总是要比访问局部变量慢,因为需要遍历作用域链。只要能减少花费在作用域链上的时间,就能增加脚本的整体性能
2.1.1、避免全局查找
可能优化脚本性能最重要的就是注意全局查找。使用全局变量和函数肯定要比局部的开销更大,因为要设计作用域链上的查找。
function updateUI() {
var imgs = document.getElementByTagName('img')
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = document.title + 'image' + i
}
var msg = document.getElementById('msg')
msg.innerHTML = 'Update complete'
}
该函数 包含了三个对于全局对象 document 的引用。for 循环中的 document 引用 可能会被执行多次,每次都会要进行作用域链查找。
通过创建一个指向 document 对象局部变量,就可以通过限制一次全局查找来改进这个函数的性能。
function updateUI() {
var doc = dcoument
var imgs = doc.getElementByTagName('img')
for (var i = 0, len = imgs.length; i < len; i++) {
imgs[i].title = doc.title + 'image' + i
}
var msg = doc.getElementById('msg')
msg.innerHTML = 'Update complete'
}
2.1.2、避免 with 语句
在性能非常重要的地方必须避免使用 with 语句。和函数类似,with 语句 会创建自己的作用域,因此会增加其中执行的代码的作用域链的长度。由于额外的作用域链查找,在 with 语句中执行的代码肯定会比外面执行的代码要慢
在大多数情况下,可以用局部变量完成相同的事情而不引入新的作用域。
function updateBody() {
with (document.body) {
alert(tagName)
innerHTML = 'Bye World!'
}
}
同样可以使用 局部变量达到相同的效果
function updateBody() {
var body = document.body
alert(body.tagName)
body.innerHTML = 'Bye World!'
}
这段代码通过 将 document.body 存储在局部变量中省却了额外的全局查找。
2.2、选择正确方法
和 其他语言一样,性能问题的一部分是和用于解决问题的算法 或者 方法 有关的。
2.2.1、避免不必要的属性查找
在计算机科学中,算法的复杂度是使用 o 符号来表示的。最简单、最快捷的算法是常数值 即 O(1)。之后,算法变得越来越复杂并花更长时间执行。
下面的表格列出了 JavaScript 中常见的算法类型
标记 | 名称 | 描述 |
---|---|---|
O(1) | 常数 | 不管有多少值,执行的事件都是恒定的。一边表示简单值和存储在变量中的值 |
O(log n) | 对数 | 总的执行时间和值的数量相关,但是要完成算法并不一定要获取每个值。例如:二分查找 |
O(n) | 线性 | 总执行时间和值的常量直接相关。例如:遍历某个数组中的所有元素 |
O(n^2) | 平方 | 总执行时间和值的数量有关,每个值至少要获取 n 次。例如:插入排序 |
符号 O(1) 表示无论有多少值,需要获取常量值的时间都一样。获取常量值是非常高效的过程。
var value = 5
var sum = 10 + value
alert(sum)
改代码进行了 四次常量值查找:数字5,变量 value,数字 10,变量 sum。这段代码的整体复杂度被认为是 O(1)
在 JavaScript 中访问数组元素 也是一个 O(1) 操作,和简单的变量查找效率一样。
var values = [5, 10]
var sum = values[0] + valuues[1]
alert(sum)
使用变量和数组要比访问对象上的数据更有效率
访问对象上的属性时 O(n)操作。对象上的任何属性查找都要比 访问变量或者数组花费更长时间,因为必须在原型链中对拥有该名称的属性进行一次搜索。简而言之,属性查找越多,执行时间就越长。
var values = {firist: 5, second: 10}
var sum = values.first + values.second
alert(sum)
这段代码使用两次属性查找计算 sum 的值。进行一两次属性查找并不会导致显著的性能问题,但是成百上千则肯定会减慢执行速度
注意获取单个值的多重属性查找
var query = window.location.href.substring(window.location.href.indexOf('?'))
这段代码中,有6次属性查找:wind.location.href.substring() 有三次,window.location.href.indexOf() 有三次。只要数一数代码中的点的数量,就可以确定属性查找的次数了。
一旦多次用到对象属性,应该将其存储来局部变量中。第一次访问值会是 O(n),然而后续的访问都会是 O(1),就会节省很多。
var url = window.location.href
var query = url.substring(url.indexOf('?'))
这个版本的代码只有四次属性查找
2.2.2、优化循环
优化循环是 性能优化过程中很重要的一部分,由于他们会反复运行同一段代码,从而自动地增加执行时间。
一个循环的基本优化步骤如下:
- 减值迭代—— 大多数循环使用一个从 0 开始、增加到某个特定值的迭代器。在很多情况下,从最大值开始,再循环中不断减值的迭代器更加高效。
- 简化终止条件——优于每次玄幻过程都会计算终止条件,所以必须保证它尽可能快。也就是说避免属性查找或其他 O(n) 的操作
- 简化循环体—— 循环体是执行最多的,所以要确保被最大限度地优化。确保没有某些可以被很容易移出循环的密集计算。
- 使用后测试循环—— 最常用 for 循环 和 while 循环都是前测试循环。而如 do-while 这种后测试循环,可以避免最初终止条件的计算,因为运行更快。
以下是一个基本 的 for 循环:
for(var i = 0; i < values.lengt; i++) {
process(values[i])
}
减值迭代
for (var i = values.length -1; i >= 0; i--) {
process[values[i]]
}
这里变量 i 每次循环之后都会减1,这个过程中,将终止条件从 value.length 的 O(n) 调用 简化成了 0 的O(1) 调用。
由于循环体只有一个语句,无法进一步优化。不过循环还能改成后测试循环:
var i = values.length -1
if (i > 1) {
do {
process(values[i])
}while (--i > 0)
}
使用“后测试”循环时必须确保要处理的值至少有一个。空数组会导致多余的一次循环而 “前测试” 循环则可以避免。
2.2.3、展开循环
当循环的次数是确定的时候,消除循环并使用多次函数调用往往更快。
// 消除循环
process(values[0])
process(values[1])
process(values[2])
如果循环中的迭代次数不能是先确定,那可以考虑使用一种叫做 Duff 装置的技术。这个技术是以 其创建者 Tom Duff 命名的。
Duff 装置的基本概念是通过计算迭代的次数是否为 8的 倍数将一个循环展开为 一系列语句。
// 假设 values.lengt > 0
var iterations = Math.ceil(values.length / 8)
var startAt = values.length % 8
var i = 0
do {
switch(startAt) {
case 0: procress(values[i++])
case 7: procress(values[i++])
case 6: procress(values[i++])
case 5: procress(values[i++])
case 4: procress(values[i++])
case 3: procress(values[i++])
case 2: procress(values[i++])
case 1: procress(values[i++])
}
startAt = 0
} while (--iterations > 0)
首先执行该循环时,会检查 startAt 变量看有需要多少额外调用。例如如果数组中有 10个 值, startAt 则等于 2,那么最开始的时候 process() 则只会被调用 2 次。在接下来的循环中,startAt 被重置为0,这样之后的每次循环都会调用 8 次 process()。展开循环可以提升大数据集的处理速度
由 Andrew B.King 所著的 Speed Up Your Site(New Riders, 2003)提出了一个更快的 Duff 装置技术,将 do-while 循环分成2个单独的循环。
var iterations = Math.ceil(values.length / 8)
var leftover = values.length % 8
var i = 0
if (leftover > 0) {
do {
process(values[i++])
} while(--leftover > 0)
}
do {
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
process(values[i++])
} while (--iterations > 0)
这个视线中,剩余的计算部分不会在实际循环中处理,而是在一个初始化循环中进行除以 8 的操作。当处理掉了额外的元素,继续执行每次调用 8 次 process() 的主循环。这个方法几乎比原始的 Duff装置实现快上 40%
针对大数据集使用展开循环 可以节省很多时间,但对于小数据集,额外的开销则可能得不偿失。它是要花更多的代码来完成同样的任务,如果处理的不是大数据集,一边来说并不值得。
2.2.4、避免双重解释
当 JavaScript 代码想解析 JavaScript 的时候就会存在双重解释惩罚。
当使用 eval() 函数 或者 是Function 构造函数以及使用 setTimeout() 传入一个字符串参数时都会发生这种情况。
// 某些代码求值—— 避免!!
eval("alert('Hello Wordl!')")
// 创建新函数——避免!!
var sayHi = new Function("alert('Hello world!')")
// 设置超时——避免!!
setTimeout("alert('Hello world!')", 500)
在这些情况下,JavaScript 代码运行的同时还必须新启动一个解析器来解析新的代码。实例化一个新的解析器有不同忽视的开销,所以这种代码要比直接解析慢得多。
对于这几个例子都有另外的办法。只有极少的情况下 eval() 是绝对必须的,所以尽可能避免使用。
// 已修正
alert('Hello World')
// 创建新函数——已修正
var sayHi = function() {
alert('Hello world!')
}
// 设置一个超市——已修正
setTimeout(function() {
alert('Hello World!')
}, 500)
如果要提高代码性能,尽可能避免出现需要按照 JavaScript 解释的字符串。
2.2.5、性能的其他注意事项
当评估脚本性能的时候,还有其他一些可以考虑的东西。下面并非主要的问题,不过如果使用得当也会有相当大的提升。
- 原生方法比较快——只要有可能,使用原生方法而不是自己用 JavaScript 重写一个。原生方法是用 诸如 C/C++ 之类的编译型语言写出来的,所以要比 JavaScript 的快很多很多。JavaScript 中最容易被忘记的就是可以在 Math 对象中找到的复杂的数学运算;这些方法要比任何 用 JavaScript 写的同样方法 如正弦、余弦快得多。
- Switch 语句较快—— 如果有一系列复杂的 if-else 语句,可以转换成单个 switch 语句则可以得到更快的代码。开可以通过将 case 语句按照最可能的到最不可能的顺序进行组织,来进一步优化 switch 语句
- 位运算符较快——当进行数学运行的时候,位运算操作要比任何布尔运算或者算数运算快。选择性地用运算替换算数运算可以极大提升复杂计算的性能。诸如取模,逻辑和逻辑或都可以考虑位运算来替换。
2.3、最小化语句数
JavaScript 代码中的语句数量也影响所执行的操作速度。完成多个操作的单个语句要比完成单个操作的多个语句块。所以,就要找出可以组合在一起的语句,以减少脚本整体的执行时间。
2.3.1、多个变量声明
有个地方很多开发人员都容易创建很多语句,那就是多个变量的声明:
// 4个语句 —— 很浪费
var count = 5
var color = 'cyan'
var values = [1, 2, 3]
var now = new Date()
在 JavaScript 中所有的变量都可以使用单个 var 语句来声明。
// 一个语句
var count = 5,
color = 'blue',
values = [1, 2, 3],
now = new Dtae()
此外,变量申明只用了一个var 语句,之间由逗号隔开。在大多数情况下这种优化都非常容易做,并且要比单个变量分别声明快很多
2.3.2、插入迭代值
当使用迭代值(也就是在不同的位置进行增加或减少的值)的时候,尽可能合并语句。
var name = values[i]
i++
这两句可以通过 迭代值插入第一个语句 组合成一个语句,如下所示:
var name = values[i++]
这一个语句可以完成和前面两个语句一样的事情。因为自增操作符是后缀操作符,i 的值只有在语句其他部分结束之后才会增加。
2.3.3、使用数组和对象字面量
使用 构造函数 总是要用到更多的语句来插入元素或者定义属性,而字面量可以将这些操作在一个语句中完成。
// 用 4 个语句创建和初始化数组 —— 浪费
var values = new Array()
values[0] = 123
values[1] = 456
values[2] = 789
// 用 4个语句创建和初始化 对象 —— 浪费
var person = new Object()
person.name = 'Lee'
person.age = 40
person.sayName = function() {
alert(this.name)
}
转换成字面量的形式
// 只用一条语句创建和初始化数组
var values = [123, 456, 789]
// 只用一条语句创建和初始化对象
var person = {
name: 'Nicholas',
age: 29,
sayName: function() {
alert(this.name)
}
}
2.4、优化 DOM 交互
在JavaScript 各个方面,DOM毫无疑问是最慢的一部分。DOM 操作与交互要消耗大量时间,因为他们往往需要重新渲染整个页面或者某一部分。理解如何优化与 DOM 的交互可以极大得提高脚本完成的速度。
2.4.1、最小化现场更新
一旦你需要访问的DOM 部分是以及显示的页面的一部分,那么你就是在进行一个现场更新(即页面对用户的显示进行更新)。每一个额更改,都有一个性能的惩罚,因为浏览器要重新计算无数尺寸以进行更新。
现场更新进行得越多,代码完成执行所花的时间就越长;完成一个操作所需的现场更新越少,代码就越快。
var list = document.getElementById('myList'),
item,
i
for (i = 0; i < 10; i++) {
item = document.createElement('li')
list.appendChild(item)
item.appendChild(document.createTextNode('item' + i))
}
这段代码为列表添加了 10 个项目。添加每个项目时,都有 2个 现场更新: 一个添加
要修正这个性能的瓶颈,一般有2个办法:
- 将列表从页面上移除,最后进行更新,最后再将列表插回到同样的位置。
这个方法不是非常理想,因为在每次页面更新的时候它会不必要的闪烁。
- 使用 文档片段来构建 DOM 结构,接着将其添加到 List 元素中。这个方式避免了现场更新和页面闪烁问题
var list = document.getElementById('myList'),
fragment = document.createDocumentFragment(),
item,
i
for (var i = 0; i < 10; i++) {
item = document.createElement('li')
fragment.appendChild(item)
item.appendChild(document.createTextNode('item' + i))
}
list.appendChild(fragment)
一旦需要更新 DOM,请考虑使用文档片段来构建 DOM 结构,然后再将其添加到现存的文档中。
2.4.2、使用 innerHTML
有两种在页面上创建 DOM 节点的方法:
- 使用 诸如 createElement() 和 appendChild() 之类的 DOM 方法
- 使用 innerHTML
对于小的DOM而言,两种方法效率都差不多。然而,对于大的 DOM 更改,使用 innerHTML 要比 使用 标准 DOM 方法创建同样的 DOM 结构快很多。
前面的例子还可以用 innerHTML 改写如下:
var list = dcoument.getElementById('myList'),
html = '',
i
for (i = 0; i < 10; i++) {
html += ' item' + i + ' '
}
list.innerHTML = html
虽然字符串连接上总是有点性能损失,但这种方式还是要比进行多个DOM操作更快。
使用 innerHTML 的关键在于(和其他 DOM 操作一样)助消化调用它的次数。
for(i = 0; i < 10; i++) {
list.innerHTML += ' item' + i + ' ' // 避免
}
2.4.3、使用事件代理
大多数 Web 应用在用户交互上大量用到事件处理程序。页面上的事件处理程序的数量和页面响应用户交互的速度之间有个负相关。为了减轻这种惩罚,最好使用事件代理。
2.4.4、注意 HTMLCollection
HTMLCollection 对于 Web 应用的性能而言是巨大的损害。任何时候要访问 HTMLCollection,不管它是一个属性还是一个方法,都是在文档上进行一个查询,这个查询开销很昂贵。最小化访问 HTMLCollection 的次数可以极大地改进脚本的性能。
也许优化 HTMLCollection 访问最重要的地方就是循环了。将长度计算移入 for 循环的初始部分
var imgs = document.getElementByTagName('img'),
i,
len
for (i = 0, len = imgs.length; i < len; i++) {
// 处理
}
这里将长度存入 len 变量,避免每次去访问 HTMLCollection的length 属性
下一步就是获取要使用的项的引用,避免在循环体内多次访问 HTMLCollection
var imgs = document.getElementByTagName('img'),
image,
i,
len
for (i = 0, len = imgs.length; i < len; i++) {
image = imgs[i]
// 处理
}
这样在后续的操作中就可以直接去访问 image 变量,而不用去再次访问 HTMLCollection
编写 JavaScript 的时候,一定要知道何时返回 HTMLCollection 对象,这样你就可以最小化对他们的访问。发生以下情况时会返回 HTMLCollection 对象
- 进行了对 getElementsByTagName() 的调用
- 获取了元素的 childNodes 属性
- 获取了元素的 attributes 属性
- 访问了特殊的集合,如 document.forms、document.images 等。
三、部署
也许所有 JavaScript 解决方案最重要的部分,便是最后部署到运营中的网站或者是 Web 应用的过程。
3.1、构建过程
完备 JavaScript 代码可以用于部署的意见很重要的事情,就是让它开发某些类型的构建过程。软件开发的典型模式是写代码—编译—测试; 在 JavaScript中模式变成了 写代码—测试,这里你写的代码就是要在浏览器中测试的代码。这个方法问题在于他不是最优的,你写的代码不应该原封不动地放入浏览器中。
理由如下:
- 知识产权问题—— 如果把带有完整注释的代码放到线上,那别人就跟容易知道你的意图了,对它再利用,并且可能找到安全漏洞。
- 文件大小—— 书写代码要保证容易阅读,才能更好地维护,但是这对于性能是不利的。浏览器并不能从额外的空白字符或者是冗长的函数名和变量名中获得什么好处
- 代码组织—— 组织代码要考虑到可维护性并不一定是是传送给浏览器的最好方式
基于这些原因,最好给 JavaScript 文件定义一个构件过程
在进行部署的时候,需要将这些源代码合并为一个或几个归并文件。推荐 Web 应用中尽可能使用最少的 JavaScript 文件,因为 HTTP 请求是 Web 中的主要性能瓶颈之一。
3.2、验证
大多数开发人员在浏览器中运行代码以检查其语法,这种发放有一些问题。
- 首先,验证过程难以自动化或者在不同系统间直接移植。
- 其次,处理语法错误外,很多问题只有在执行代码的时候才会遇到,这给错误留下来空间;
JSLink 可以查找 JavaScript 代码中的语法错误以及常见的编码错误。它可以发掘的一些潜在问题如下:
- eval()的使用
- 未声明变量的使用
- 遗漏的分号
- 不恰当的换行
- 错误的逗号使用
- 语句周围遗漏的括号
- switch 分支语句中 遗漏的 break
- 重复声明的变量
- 错误使用的等号
- 无法到达的代码
给开发周期添加代码验证 这个环节有助于避免将来可能出现的一些错误。建议开发人员给构建过程加入某种类型的代码验证作为确定潜在问题的一个方法,防范于未然。
3.3、压缩
当谈及 JavaScript 文件压缩,其实在讨论两个东西:代码长度和配置(Wire weight)。代码长度值的是浏览器所需解析的字节数,配种指的是从实际服务器传送到浏览器的字节数。
3.3.1、文件压缩
压缩器一般进行如下一些步骤:
- 删除额外的空白(包括换行)
- 删除所有注释
- 缩短变量名
3.3.2、HTTP压缩
配种指的是实际从服务器传送到浏览器的字节数。因为现有的服务器和浏览器都有压缩功能,这个字节数不一定和代码长度一样。五大 Web 浏览器都支持对所接收的资源进行客户端解压压缩。
一个指定李文建使用了给定格式进行了压缩的 HTTP 头包含在了 服务器响应中。接着浏览器会查看该 HTTP 头确定文件是否已被压缩,然后使用 合适的格式进行解压缩。结果是和原来的代码量相比在网络中传递的字节数量大大减少了。
对于 Apache Web 服务器,有两个模块可以进行 HTTP 压缩:mod_gzip 和 mod_deflate
mod_gzip 可以给 httpd.conf 文件或者是 .htaccess 文件添加如下代码启用对 JavaScript的自动压缩
# 告诉 mod_zip 要包含任何以 .js 结尾的文件
mod_gzip_item_include file \.js
mod_deflate 可以类似添加一行代码以保证 JavaScript 文件在被发送之前已被压缩。将以下这一行代码添加到 http.conf 文件或者是 .htaccess 文件中
# 告诉 mod_deflate 要包含所有的 JavaScript 文件
AddOutputFilterByType DEFLATE applocation/x-javascript
JavaScript 的 MIME 类型是 application/x-javascript
mode_gzip 和 mod_deflate 都可以节省大约 70% 的 JavaScript文件大小。记住有一点点围绕的代价,因为服务器必须花时间对每个请求压缩文件,当浏览器接受到这些文件后也需要花一些时间解压缩。不过,一般来说,这个代价还是值得的。
四、小结
JavaScript中的可维护性部分涉及到下面的代码约定
- 由于 JavaScript 必须与 HTML 和 CSS 共存,所以让各自完全定义其自己的目的非常重要:JavaScript的应该定义行为,HTML 应该定义内容,CSS应该定义外观
- 这些职责的混淆会导致难以调试的错误和维护上的问题。
性能
- JavaScript执行所花费的事件直接影响到整个 Web 页面的性能,所以其重要性是不能忽略的。
- 针对基于 C 的语言的很多性能的建议也适用于 JavaScript,如有关循环性能和 使用 switch 语句 替代 if 语句。
- DOM 交互开销 很大,所以需要限制 DOM 操作的次数
部署
- 为了协助部署,推荐设置一个可以将 JavaScript 合并为较少文件(理想情况是一个)的构
建过程 - 有了构建过程也可以对源码自动进行额外的处理和过滤
- 在部署前推荐使用压缩器将文件尽可能变小
- 和 HTTP 压缩一起使用 可以让 JavaScript 文件尽可能小,因此对整体页面性能的影响也会最小。