写出快速,节省内存的Javascript代码(二)

写出快速,节省内存的Javascript代码(二)

写出快速,节省内存的Javascript代码(一)

小心性能陷阱

别为优化代码而去优化代码,这点越重视越好.在V8的代码中你很容易看到一些N方法比M方法更高效的地方,但是在某个模块或者整个程序上来看这些优化并不是你所期望的那么有作用.

过犹不及

过犹不及

例如我们要创建这样一个模块:

  • 获取包含数字id值的本地数据
  • 绘制一个table来存放这些数据
  • 增加事件监听以便用户点击任意一个单元格都可以触发一个响应.

尽管这个问题很简单,仍有一些要注意的地方:怎样存储数据?怎样高效地绘制table并加入的DOM中?怎样高效地处理这个table触发的事件.

对这个问题最直接的方法是把数据放在数组中,然后用jquery遍历这些数据并绘制table再加到DOM中.最后,对单击增加事件绑定处理就可以了.

注意:以下代码是错误的例子

 1     var moduleA = function () {
 2         return {
 3             data: dataArrayObject,
 4             init: function () {
 5             this.addTable();
 6             this.addEvents();
 7            },
 8             addTable: function () {
 9
10                 for (var i = 0; i < rows; i++) {
11                     $tr = $('<tr></tr>');
12                     for (var j = 0; j < this.data.length; j++) {
13                        $tr.append('<td>' + this.data[j]['id'] + '</td>');
14                     }
15                     $tr.appendTo($tbody);
16                 }
17             },
18             addEvents: function () {
19                 $('table td').on('click', function () {
20                    $(this).toggleClass('active');
21                 });
22             }
23         };
24     }();
25

就这样就可以完成我们的任务.
然而在这个例子中,我们使用的数据只有id值,这个数值型的属性放在一个标准的数组中来描述会更简单.有意思的是,直接使用DocumentFragment和DOM原生的方法会比使用jquery来创建table更高效.并且使用事件委托也会比依次绑定每一个td要更好.

注意,在jquery内部并没有使用DocumentFragment,在我们的例子中,代码在循环中调用append()方法并且相互之间没有关联,所以并没有优化的空间.这并不是一个棘手的事情,但是确保你的代码能够通过测试.

这个例子中,修改这些东西可以提高性能.事件委托比简单的事件绑定要高效的多,并且使用DocumentFragment会有很大的提高.

 1    var moduleD = function () {
 2          return {
 3              data: dataArray,
 4              init: function () {
 5                  this.addTable();
 6                  this.addEvents();
 7              },
 8              addTable: function () {
 9                  var td, tr;
10                  var frag = document.createDocumentFragment();
11                  var frag2 = document.createDocumentFragment();
12                  for (var i = 0; i < rows; i++) {
13                      tr = document.createElement('tr');
14                      for (var j = 0; j < this.data.length; j++) {
15                          td = document.createElement('td');
16                          td.appendChild(document.createTextNode(this.data[j]));
17                          frag2.appendChild(td);
18                      }
19                      tr.appendChild(frag2);
20                      frag.appendChild(tr);
21                  }
22                  tbody.appendChild(frag);
23              },
24              addEvents: function () {
25                  $('table').on('click', 'td', function () {
26                      $(this).toggleClass('active');
27                  });
28              }
29          };
30    }();

我们还可以从其他方面来提升性能.你或许听到过使用原型模式要比模块模式高效,使用javascript的模版框架更加高效.有些时候它们是高效,但是使用它们的原因也是因为它们易读.并且是预编译的!我们来测试下它的实际表现:

 1
 2    moduleG = function () {};
 3    moduleG.prototype.data = dataArray;
 4    moduleG.prototype.init = function () {
 5          this.addTable();
 6          this.addEvents();
 7    };
 8    moduleG.prototype.addTable = function () {
 9          var template = _.template($('#template').text());
10          var html = template({'data' : this.data});
11          $tbody.append(html);
12    };
13
14
15    moduleG.prototype.addEvents = function () {
16         $('table').on('click', 'td', function () {
17             $(this).toggleClass('active');
18         });
19    };
20
21    var modG = new moduleG();

事实表明,这种方式对效率的提升是微不足道的.[使用模版和原型]并不比我们之前的做法好多少.也就是说,性能并不是优秀的开发者选择他们的原因,选择他们是因为这种继承模型和易维护所带来的易读性.

其他的问题包括使用canvas高效地绘制图片和使用类型化数组操作像素数据

在使用一些所谓的标准时最后要先检测一遍再应用的你的程序中.你可以参考不同JavaScript模版引擎性能预览对其扩展.你需要弄清楚测试并没有按照实际部署的应用程序中哪些不常见的规范来压缩,它仅仅是用代码来优化的.

V8的优化要点

尽管V8的优化要点有很多值得记录下来的,但是一一罗列出来并不是本文的目标.把这些记住就可以帮你减少写出低效率的代码:

  • 一些模式会让V8陷入无法优化的困境.例如,一个trycach就可以(**译者注:V8的优化器不支持trycach结构**).关于那些方法结构可以被优化那些不能被优化可以通过V8的一个命令行工具d8执行 --trace-opt 要测试的js文件.js来获取.
  • 如果你很关心速度,尽量保持你的函数功能单一.例如,确保变量(包括属性、数组和函数参数)只包含相同的对象隐藏类。例如,不要这样做:
1        function add(x, y) { 
2            return x+y;
3        } 
4        add(1, 2); 
5        add('a','b'); 
6        add(my_custom_object, undefined);//(*在V8中这两个是不同的hidden class*)
  • 不要加载没有初始化或者被删除的元素.这在输出上并没有什么问题,但是却会人程序很慢.
  • 不用写巨大的方法,因为他们优化起来很困难.

关于更多的要点,参看Daniel Clifford在Google I/O大会上的演讲: Breaking the JavaScript Speed Limit with V8,(youtube)国内优酷地址.V8优化系列也值得一读.

objects vs arrays:我该用那个?
  • 如果你想存储一串数字,或者一列相同类型的对象(object),使用array.
  • 如果你在语义上是需要一个包含很多不同类型的属性的对象,就用object并设置属性.这在内存上是很高效的,并且也很快.
  • 整数索引的元素,不管它是被存在数组array中还是对象object中,在迭代是比用对象object属性快
  • object中的属性相当的复杂:它们可以被setters来设置创建,可以有不同的枚举性并且可写.而在数组中的元素就不能被过分的定制了--它们要么存在,要么不存在.object的这种特性在引擎层面就需要在内存组织上对描述这种结构进行更多的优化.包含数值的数组会更好,例如当你需要一系列向量时,别定义一个包含x,y,z属性的类;直接使用array...

object和array事实上只有一个最重要的区别:那就是array的魔法属性length.如果你自己在object中记录这个属性,那么在V8中object和array是一样快的.

使用对象(object)的注意点:
  • 使用构造方法来创建对象:这样可以确保所有通过它创建出来的对象具有相同的hidden class,并且避免改变这些隐藏类。并且这样做也比直接使用Object.create()
  • 尽管在程序中使用多少个对象类型并没有限制,但是太长的属性链还是会影响性能。并且具有少量属性的对象比大的对象要高效的多。对与一些频繁使用的对象,应该尽量让它的属性链和属性个数少。
对象拷贝

对象拷贝是每个开发者都会遇到的问题。尽管可以一一罗列出V8在处理这个问题上的很多优秀的处理方法,但是还是要做谨慎地拷贝。拷贝大的对象时特别的慢,所有尽量别这样做。在JavaScript中for..in的循环不管任何引擎去实现都很慢,因为它的技术规范很糟糕……
如果你必须要拷贝一个对象,用一个数组或者定制的“拷贝构造”方法来拷贝每一个属性。这应该是最快的方式了。

1    function clone(original) {
2    this.foo = original.foo;
3    this.bar = original.bar;
4    }
5    var copy = new clone(original);
在模块中使用缓存方法

在模块中使用缓存方法可以提供效率。下边的例子能说明我们平常使用变量的方法是很慢的,因为它总是拷贝成员方法。

module_pattern

 1
 2      // Prototypal pattern
 3        Klass1 = function () {}
 4        Klass1.prototype.foo = function () {
 5            log('foo');
 6        }
 7        Klass1.prototype.bar = function () {
 8            log('bar');
 9        }
10
11        // Module pattern
12        Klass2 = function () {
13            var foo = function () {
14                log('foo');
15            },
16            bar = function () {
17                log('bar');
18            };
19
20            return {
21                foo: foo,
22                bar: bar
23            }
24        }
25
26
27        // Module pattern with cached functions
28        var FooFunction = function () {
29            log('foo');
30        };
31        var BarFunction = function () {
32            log('bar');
33        };
34
35        Klass3 = function () {
36            return {
37                foo: FooFunction,
38                bar: BarFunction
39            }
40        }
41
42
43        // Iteration tests
44
45        // Prototypal
46        var i = 1000,
47            objs = [];
48        while (i--) {
49            var o = new Klass1()
50            objs.push(new Klass1());
51            o.bar;
52            o.foo;
53        }
54
55        // Module pattern
56        var i = 1000,
57            objs = [];
58        while (i--) {
59            var o = Klass2()
60            objs.push(Klass2());
61            o.bar;
62            o.foo;
63        }
64
65        // Module pattern with cached functions
66        var i = 1000,
67            objs = [];
68        while (i--) {
69            var o = Klass3()
70            objs.push(Klass3());
71            o.bar;
72            o.foo;
73        }
74

注意:当你并不需要一个类时就别用。你可以通过这个链接来了解怎样通过去除类来提高性能的http://jsperf.com/prototypal-performance/54

使用数组的注意点:

接下来我们看看使用数组的注意点。一般来说,不用删除数组的元素。这会让数组内部响应越来越慢。当设置的关键字变得”疏松“,V8会把它转向比较慢的字典模式。

数组字面量(Array Literals)

Array Literals 很有用,因为它告诉解释器数组的类型和大小,这对中小型数组很有用。

1    // Here V8 can see that you want a 4-element array containing numbers:
2    var a = [1, 2, 3, 4];
3
4    // Don't do this:
5    a = []; // Here V8 knows nothing about the array
6    for(var i = 1; i <= 4; i++) {
7           a.push(i);
8    }
单类型存储还是混合存储?

最后不要把不同类型(如数值,字符串,undefined或者布尔)的元素放在同一个数组中(例如:var arr = [1, “1”, undefined, true, “true”])

类型影响测试

从结果中我们可以看到,使用纯int的数组要快些。

稀疏的数组与完整的数组

当你使用稀疏数组时要注意,获取它的要素的速度要不一个完整的数组要慢。这是因为V8并不会因为仅仅少数几个元素在使用而压缩存储。V8用字典的方式来节省空间,但这样在获取上要稍微费时点。
稀疏数组和完整数组的测试

译者注:所谓的稀疏数组简单地说就是定义了var arr=[]之后,通过arr[0]=100;arr[100]=1000这种间断地赋值造成数组中有未真正赋值的元素

在完整数组的求和运算中,不管是否包含0其计算速度大概是相同的。

打包的数组和带”洞“的数组

避免使用带”洞“的数组(删除了数组的元素或者通过下标大于长度的方式赋值)。即使只有一个元素被删除了,整个数组都会变慢。
打包的数组和带”洞“的数组的测试

预先分配数组空间还是逐渐增加

不用一下就预先分配大的数组(比如元素超过64k),而要让其自增。在测试之前请注意:这一点只适用于部分javascript引擎。
graph2
Test of empty literal versus pre-allocated array in various browsers.

Nitro(Safari)对预先分配数组容量提供了很好的支持,而其他的引擎(V8,SpiderMonkey)不预先分配会更好。
预先分配的测试

 1    // Empty array
 2    var arr = [];
 3    for (var i = 0; i < 1000000; i++) {
 4          arr[i] = i;
 5    }
 6
 7    // Pre-allocated array
 8    var arr = new Array(1000000);
 9    for (var i = 0; i < 1000000; i++) {
10          arr[i] = i;
11    }

Powered by Engin & toto

comments powered by Disqus