App之性能优化

一般来说,浏览器的内存泄漏对于 web 应用程序来说并不是什么问题。用户在页面之间切换,每个页面切换都会引起浏览器刷新。即使页面上有内存泄漏,在页面切换后泄漏就解除了。由于泄漏的范围比较小,因此常常被忽视。

但在移动端,内存泄漏就成了一个比较严重的问题。在单面应用中,用户不能刷新页面的,整个应用程序构建在一个页面上。在这种情况下泄漏会被累积,导致内存不被回收。

Javascript中的垃圾回收机制类似于Java/C#这类语言中的回收机制:

一个对象不再被引用,即将被自动回收

具体回收时刻是我们无法控制的,我们只需适当地解除对象的引用,剩下的事,让运行时去做吧。

在我们开发过程中,往往稍不留神,内存泄露了我们可能都不会察觉:

例1:

1 function doFn(){
2    bigString=new Array(1000).join(new Array(2000).join("XXXXX"));
3 }

不论是你不小心少写了个var,还是觉得这样写很cool,执行doFn(),即退出函数作用域后,bigString会被回收掉么?

不会被回收,bigString现在成为了全局对象window的一个属性,在应用的整个生命周期,window都是一直存在的,所以其属性是不会被销毁的。

例2:

1 var doFn=(function(){ 
2 var bigString=new Array(1000).join(new Array(2000).join("XXXXX")); 
3    return  function(){
4       console.dir(bigString);
5    } ; 
6 })();

上面代码运行后,bigString会被回收么?

不会被回收,闭包里的数据是不会被释放的。

例3:

<intput type=”button” value=”submit”  id=”submit” />
1 (function(){
2   var Zombie=function(){};
3   var zombie=new Zombie;
4   var print=function(){
5      console.dir(zombie);
6   };
7   var node=document.getElementById(‘submit’);
8   node.addEventListener('click',print,false);
9 })()

运行代码后,事件处理函数执行正常,会打印zombie到控制台,而且这里会发生内存泄露,zombie一直不能被回收。

也许有人会说,离开这个页面,zombie就会被释放。在单页应用中,离开当前页面,实质是,移除页面上body内的所有DOM元素,然后再把新的HTML追加至body的DOM树上。

所以,我们来移除button这个节点:

1 node.parentNode.removeChild(node);

执行之后,我们发现页面上按钮被移除了。现在,zombie对象应该被回收了吧?

我们用chrome浏览器的Heap Profiler来追踪下内存,下面是内存快照:

发现即使移除DOM节点,内存泄露一样存在。当我们在移除元素的同时移除其上的事件时,发现这次zombie被回收了:

1 node.parentNode.removeChild(node);
2 node.removeEventListener(‘click’,print,false);

再次追踪内存,已经没有在Zombie类型的对象遗留在内存中了。

所以,我们得出一个结论:移除一个DOM元素的同时,也要移除元素上面的事件,不然很可能会发生内存泄露,伤你于无形。

说到这里,我就想起了zepto里的移除元素的remove方法:

1 remove: function(){
2   return this.each(function(){
3      if(this.parentNode != null)
4        this.parentNode.removeChild(this)
5   })
6 }

说好的要移除元素上面的事件呢?

另外我们对比下zepto和jQuery里的empty方法:

zepto的empty方法:

1 empty: function(){
2      return this.each(function(){ this.innerHTML = '' })
3 }

jQuery的empty方法:

 1 empty: function() {
 2     var elem,i = 0;
 3     for ( ; (elem = this[i]) != null; i++ ) {
 4            if ( elem.nodeType === 1 ) {
 5            // Prevent memory leaks
 6            jQuery.cleanData( getAll( elem, false ) );
 7            // Remove any remaining nodes
 8            elem.textContent = "";
 9         }
10     }
11     return this;
12 }

API文档里还有这么一句话:

To avoid memory leaks, jQuery removes other constructs such as data and event handlers from the child elements before removing the elements themselves.

可见,对于移除DOM元素时,jQuery处理要更为严谨和合理。

在模块化编程时,当我们会用RequireJS来组织代码时,有一种情况是需要注意的:

1 define([],function(){
2    var obj={
3        bigString:new Array(1000).join(new Array(2000).join("XXXXX"));
4        //
5    };
6    return obj;
7 });

当这个模块作为一个数据源时,在某个地方被加载一次后,即时当前视图已不再需要它,它还会一直保留在内存中。也就是说,返回值为一个对象时,它是不会被释放的。

至于为何这样,你可以想想,我们define一个类后,能通过require来调用它,那么它肯定是在什么地方被保存了起来。所以,我们这个obj在RequireJs内部也会被引用,无法释放。

也许你会问,那你干嘛要返回一个对象呢?我想,有时候,你应该也是这么做的。

另外,不知道大家的Controller层是如何写的,我是让它继承Backbone.Router的:

 1  jass.Controller = Backbone.Router.extend({
 2         module: "",
 3         name: "",
 4         _bindRoutes: function () {
 5             if (!this.routes) return;
 6             this.routes = _.result(this, 'routes');
 7             var route, routes = _.keys(this.routes);
 8             var prefix = this.module + "/" + this.name + "/";
 9             while ((route = routes.pop()) != null) {
10                 this.route(prefix + route, this.routes[route]);
11             }
12         },
13         close: function () {
14             // destory
15             // remove actions from history.Handlers ???
16             this.stopListening();
17             this.off();
18             this.trigger('destroy');
19         }
20 });

这样写也会内存泄露,我们跟踪下router方法:

1 this.route(prefix + route, this.routes[route]);  // this -->controller

controller被引用了,它是无法释放的。如果在Controller层上面再引用了Model层表示的数据,泄露将会更加严重。

另外,我这里企图作一些清理工作的close方法根本就没有时机去触发。

我们简化Controller逻辑,它只负责向View层传递Model层的数据时,在多数情况下是会降低泄露的发生。

但是,我们经常会面临这样的问题:

1 多个View之间共享数据;

2 多个Controller之间共享数据;

这时数据应该保存在哪,该何时被清理掉?

为了解决上面的问题,我希望从AngularJS中能得到一些启发,发现它的概念还是挺多的。然后找到AngularJS中依赖注入的模拟代码:

 1 var angular = function(){};
 2  
 3 Object.defineProperty(angular,"module",{
 4     value:function(modulename,args){
 5         var module = function(){
 6             this.args = args;
 7             this.factoryObject = {};
 8             this.controllerObject = {};
 9         }
10         module.prototype.factory = function(name,service){
11             //if service is not a function ... 
12             //if service() the result is not a object ... and so on
13             this.factoryObject[name] = service();
14         }
15         module.prototype.controller = function(name,args){
16             var _self  = this;
17             //init
18             var content = {
19                 $scope:{},
20                 scope:function(){
21                     return content.$scope;
22                 }
23             //  $someOther:{...}
24             }
25  
26             var ctrl = args.pop();
27             console.log(typeof ctrl);
28             var factorys = [];
29             while(service = args.shift()){
30                 if(service in content){
31                     factorys.push(content[service])
32                 }else{
33                     factorys.push(_self.factoryObject[service])
34                 }
35                  
36             }
37             ctrl.apply(null,factorys);
38  
39             _self.controllerObject[name] = function(){
40                 return content;
41             };
42         }
43         var m = new module();
44         window[modulename] = m;
45         return m;
46     }
47 })

测试:

 1 var hello = angular.module('Test');
 2  
 3 hello.factory("actionService",function(){
 4     var say = function(){
 5         console.log("hello")
 6     }
 7     return {
 8         "say":say
 9     }
10 })
11  
12 hello.controller("doCtrl",['$scope',"actionService",function($scope,actionService){
13     $scope.do = function(){
14         actionService.say();
15     }
16 }]);
17  
18 hello.controllerObject.doCtrl().scope().do()

可见,AngularJS中构造的模块,控制器也是不会被释放的。

在单页应用开发中,更要警惕内存泄露问题,不然它会是性能优化的一个巨大绊脚石。

性能优化,是一个永久的话题,以后有所感悟,再来补充,持续更新!

最近在研究Sencha Touch,期待有趣的发现!

更多有关性能优化的讨论,推荐阅读:

Memory leaks

Memory leak patterns in JavaScript

Writing Fast,Memory-Efficient JavaScript

Backbone.js And JavaScript Garbage Collection

雅虎网站页面性能优化的34条黄金守则

 

posted @ 2014-09-02 11:25 秦时V明月 阅读(...) 评论(...) 编辑 收藏