App之性能優化

通常來講,瀏覽器的內存泄漏對於 web 應用程序來講並非什麼問題。用戶在頁面之間切換,每一個頁面切換都會引發瀏覽器刷新。即便頁面上有內存泄漏,在頁面切換後泄漏就解除了。因爲泄漏的範圍比較小,所以經常被忽視。javascript

但在移動端,內存泄漏就成了一個比較嚴重的問題。在單面應用中,用戶不能刷新頁面的,整個應用程序構建在一個頁面上。在這種狀況下泄漏會被累積,致使內存不被回收。html

Javascript中的垃圾回收機制相似於Java/C#這類語言中的回收機制:java

一個對象再也不被引用,即將被自動回收node

具體回收時刻是咱們沒法控制的,咱們只需適當地解除對象的引用,剩下的事,讓運行時去作吧。jquery

在咱們開發過程當中,每每稍不留神,內存泄露了咱們可能都不會察覺:web

例1:chrome

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

不管是你不當心少寫了個var,仍是以爲這樣寫很cool,執行doFn(),即退出函數做用域後,bigString會被回收掉麼?編程

不會被回收,bigString如今成爲了全局對象window的一個屬性,在應用的整個生命週期,window都是一直存在的,因此其屬性是不會被銷燬的。

例2:api

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條黃金守則

相關文章
相關標籤/搜索