通常來講,瀏覽器的內存泄漏對於 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 leak patterns in JavaScript
Writing Fast,Memory-Efficient JavaScript