<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                合規國際互聯網加速 OSASE為企業客戶提供高速穩定SD-WAN國際加速解決方案。 廣告
                \# 常見的JavaScript內存泄露 !\[\](./images/head.jpg) \## 什么是內存泄露 \> \*\*內存泄漏\*\*指由于疏忽或錯誤造成程序未能釋放已經不再使用的內存。內存泄漏并非指內存在物理上的消失,而是應用程序分配某段內存后,由于設計錯誤,導致在釋放該段內存之前就失去了對該段內存的控制,從而造成了內存的浪費。 \>內存泄漏通常情況下只能由獲得程序源代碼的程序員才能分析出來。然而,有不少人習慣于把任何不需要的內存使用的增加描述為內存泄漏,即使嚴格意義上來說這是不準確的。 ————\[wikipedia\](https://zh.wikipedia.org/wiki/%E5%86%85%E5%AD%98%E6%B3%84%E6%BC%8F) \*\*??注:下文中標注的CG是Chrome瀏覽器中Devtools的【Collect garbage】按鈕縮寫,表示回收垃圾操作。\*\* !\[cg\](https://raw.githubusercontent.com/zhansingsong/js-leakage-patterns/master/images/CG.png) \## 意外的全局變量 JavaScript對未聲明變量的處理方式:在全局對象上創建該變量的引用(即全局對象上的屬性,不是變量,因為它能通過`delete`刪除)。如果在瀏覽器中,全局對象就是\*\*window\*\*對象。 如果未聲明的變量緩存大量的數據,會導致這些數據只有在窗口關閉或重新刷新頁面時才能被釋放。這樣會造成意外的內存泄漏。 ```js function foo(arg) { bar = "this is a hidden global variable with a large of data"; } ``` 等同于: ```js function foo(arg) { window.bar = "this is an explicit global variable with a large of data"; } ``` 另外,通過\*\*this\*\*創建意外的全局變量: ```js function foo() { this.variable = "potential accidental global"; } // 當在全局作用域中調用foo函數,此時this指向的是全局對象(window),而不是'undefined' foo(); ``` \### 解決方法: 在JavaScript文件中添加`'use strict'`,開啟嚴格模式,可以有效地避免上述問題。 ```js function foo(arg) { "use strict" // 在foo函數作用域內開啟嚴格模式 bar = "this is an explicit global variable with a large of data";// 報錯:因為bar還沒有被聲明 } ``` 如果需要在一個函數中使用全局變量,可以像如下代碼所示,在\*\*window\*\*上明確聲明: ```js function foo(arg) { window.bar = "this is a explicit global variable with a large of data"; } ``` 這樣不僅可讀性高,而且后期維護也方便 \> 談到全局變量,需要注意那些用來臨時存儲大量數據的全局變量,確保在處理完這些數據后將其設置為null或重新賦值。全局變量也常用來做cache,一般cache都是為了性能優化才用到的,為了性能,最好對cache的大小做個上限限制。因為cache是不能被回收的,越高cache會導致越高的內存消耗。 \## console.log `console.log`:向web開發控制臺打印一條消息,常用來在開發時調試分析。有時在開發時,需要打印一些對象信息,但發布時卻忘記去掉`console.log`語句,這可能造成內存泄露。 在傳遞給`console.log`的對象是不能被垃圾回收 ??,因為在代碼運行之后需要在開發工具能查看對象信息。所以最好不要在生產環境中`console.log`任何對象。 \### 實例------>\[demos/log.html\](./demos/log.html) ```html Leaker !function () { function Leaker() { this.init(); }; Leaker.prototype = { init: function () { this.name = (Array(100000)).join('\*'); console.log("Leaking an object %o: %o", (new Date()), this);// this對象不能被回收 }, destroy: function () { // do something.... } }; document.querySelector('input').addEventListener('click', function () { new Leaker(); }, false); }() ``` 這里結合Chrome的Devtools–>Performance做一些分析,操作步驟如下: \*\*:warning:注:最好在隱藏窗口中進行分析工作,避免瀏覽器插件影響分析結果\*\* 1. 開啟【Performance】項的記錄 2. 執行一次CG,創建基準參考線 3. 連續單擊【click】按鈕三次,新建三個Leaker對象 4. 執行一次CG 5. 停止記錄 !\[\](./images/console\_1.png) 可以看出【JS Heap】線最后沒有降回到基準參考線的位置,顯然存在沒有被回收的內存。如果將代碼修改為: ```js !function () { function Leaker() { this.init(); }; Leaker.prototype = { init: function () { this.name = (Array(100000)).join('\*'); }, destroy: function () { // do something.... } }; document.querySelector('input').addEventListener('click', function () { new Leaker(); }, false); }() ``` 去掉`console.log("Leaking an object %o: %o", (new Date()), this);`語句。重復上述的操作步驟,分析結果如下: !\[\](./images/console\_2.png) 從對比分析結果可知,`console.log`打印的對象是不會被垃圾回收器回收的。因此最好不要在頁面中`console.log`任何大對象,這樣可能會影響頁面的整體性能,特別在生產環境中。除了`console.log`外,另外還有`console.dir`、`console.error`、`console.warn`等都存在類似的問題,這些細節需要特別的關注。 \## closures(閉包) 當一個函數A返回一個內聯函數B,即使函數A執行完,函數B也能訪問函數A作用域內的變量,這就是一個閉包——————本質上閉包是將函數內部和外部連接起來的一座橋梁。 ```js function foo(message) { function closure() { console.log(message) }; return closure; } // 使用 var bar = foo("hello closure!"); bar()// 返回 'hello closure!' ``` 在函數foo內創建的函數closure對象是不能被回收掉的,因為它被全局變量bar引用,處于一直可訪問狀態。通過執行`bar()`可以打印出`hello closure!`。如果想釋放掉可以將`bar = null`即可。 \*\*由于閉包會攜帶包含它的函數的作用域,因此會比其他函數占用更多的內存。過度使用閉包可能會導致內存占用過多。\*\* \### 實例------>\[demos/closures.html\](./demos/closures.html) ```html Closure 不斷單擊【click】按鈕 Click function f() { var str = Array(10000).join('#'); var foo = { name: 'foo' } function unused() { var message = 'it is only a test message'; str = 'unused: ' + str; } function getData() { return 'data'; } return getData; } var list = \[\]; document.querySelector('#click\_button').addEventListener('click', function () { list.push(f()); }, false); ``` 這里結合Chrome的Devtools->Memory工具進行分析,操作步驟如下: \*\*:warning:注:最好在隱藏窗口中進行分析工作,避免瀏覽器插件影響分析結果\*\* 1. 選中【Record allocation timeline】選項 2. 執行一次CG 3. 單擊【start】按鈕開始記錄堆分析 3. 連續單擊【click】按鈕十多次 4. 停止記錄堆分析 !\[closure\](./images/closure1.png) 上圖中藍色柱形條表示隨著時間新分配的內存。選中其中某條藍色柱形條,過濾出對應新分配的對象: !\[closure\](./images/closure2.png) 查看對象的詳細信息: !\[closure\](./images/closure3.png) 從圖可知,在返回的閉包作用鏈(Scopes)中攜帶有它所在函數的作用域,作用域中還包含一個str字段。而str字段并沒有在返回getData()中使用過。為什么會存在在作用域中,按理應該被GC回收掉, why:question: 原因是在相同作用域內創建的多個內部函數對象是共享同一個\[變量對象(variable object)\](http://dmitrysoshnikov.com/ecmascript/chapter-2-variable-object/)。如果創建的內部函數沒有被其他對象引用,不管內部函數是否引用外部函數的變量和函數,在外部函數執行完,對應變量對象便會被銷毀。反之,如果內部函數中存在有對外部函數變量或函數的訪問(可以不是被引用的內部函數),并且存在某個或多個內部函數被其他對象引用,那么就會形成閉包,外部函數的變量對象就會存在于閉包函數的作用域鏈中。這樣確保了閉包函數有權訪問外部函數的所有變量和函數。了解了問題產生的原因,便可以對癥下藥了。對代碼做如下修改: ```js function f() { var str = Array(10000).join('#'); var foo = { name: 'foo' } function unused() { var message = 'it is only a test message'; // str = 'unused: ' + str; //刪除該條語句 } function getData() { return 'data'; } return getData; } var list = \[\]; document.querySelector('#click\_button').addEventListener('click', function () { list.push(f()); }, false); ``` getData()和unused()內部函數共享f函數對應的變量對象,因為unused()內部函數訪問了f作用域內str變量,所以str字段存在于f變量對象中。加上getData()內部函數被返回,被其他對象引用,形成了閉包,因此對應的f變量對象存在于閉包函數的作用域鏈中。這里只要將函數unused中`str = 'unused: ' + str;`語句刪除便可解決問題。 !\[closure\](./images/closure4.png) 查看一下閉包信息: !\[closure\](./images/closure5.png) \## DOM泄露 在JavaScript中,DOM操作是非常耗時的。因為JavaScript/ECMAScript引擎獨立于渲染引擎,而DOM是位于渲染引擎,相互訪問需要消耗一定的資源。如Chrome瀏覽器中DOM位于WebCore,而JavaScript/ECMAScript位于V8中。假如將JavaScript/ECMAScript、DOM分別想象成兩座孤島,兩島之間通過一座收費橋連接,過橋需要交納一定“過橋費”。JavaScript/ECMAScript每次訪問DOM時,都需要交納“過橋費”。因此訪問DOM次數越多,費用越高,頁面性能就會受到很大影響。\[了解更多:information\_source:\](http://www.phpied.com/dom-access-optimization/) !\[\](http://www.phpied.com/wp-content/uploads/2009/12/domlandia.png) 為了減少DOM訪問次數,一般情況下,當需要多次訪問同一個DOM方法或屬性時,會將DOM引用緩存到一個局部變量中。但如果在執行某些刪除、更新操作后,可能會忘記釋放掉代碼中對應的DOM引用,這樣會造成DOM內存泄露。 \### 實例------>\[demos/dom.html\](./demos/dom.html) ```html Dom-Leakage // 因為要多次用到pre.wrapper、div.container、input.remove、input.add節點,將其緩存到本地變量中, var wrapper = document.querySelector('.wrapper'); var container = document.querySelector('.container'); var removeBtn = document.querySelector('.remove'); var addBtn = document.querySelector('.add'); var counter = 0; var once = true; // 方法 var hide = function(target){ target.style.display = 'none'; } var show = function(target){ target.style.display = 'inline-block'; } // 回調函數 var removeCallback = function(){ removeBtn.removeEventListener('click', removeCallback, false); addBtn.removeEventListener('click', addCallback, false); hide(addBtn); hide(removeBtn); container.removeChild(wrapper); } var addCallback = function(){ wrapper.appendChild(document.createTextNode('\\t' + ++counter + ':a new line text\\n')); // 顯示刪除操作按鈕 if(once){ show(removeBtn); once = false; } } // 綁定事件 removeBtn.addEventListener('click', removeCallback, false); addBtn.addEventListener('click', addCallback, false); ``` 這里結合Chrome瀏覽器的Devtools–>Performance做一些分析,操作步驟如下: \*\*:warning:注:最好在隱藏窗口中進行分析工作,避免瀏覽器插件影響分析結果\*\* 1. 開啟【Performance】項的記錄 2. 執行一次CG,創建基準參考線 3. 連續單擊【add】按鈕6次,增加6個文本節點到pre元素中 4. 單擊【remove】按鈕,刪除剛增加6個文本節點和pre元元素 5. 執行一次CG 6. 停止記錄堆分析 !\[dom\](./images/dom1.png) 從分析結果圖可知,雖然6次add操作增加6個Node,但是remove操作并沒有讓Nodes節點數下降,即remove操作失敗。盡管還主動執行了一次CG操作,Nodes曲線也沒有下降。因此可以斷定內存泄露了!那問題來了,如何去查找問題的原因呢?這里可以通過Chrome瀏覽器的Devtools–>Memory進行診斷分析,執行如下操作步驟: \*\*:warning:注:最好在隱藏窗口中進行分析工作,避免瀏覽器插件影響分析結果\*\* 1. 選中【Take heap snapshot】選項 2. 連續單擊【add】按鈕6次,增加6個文本節點到pre元素中 3. 單擊【Take snapshot】按鈕,執行一次堆快照 4. 單擊【remove】按鈕,刪除剛增加6個文本節點和pre元元素 5. 單擊【Take snapshot】按鈕,執行一次堆快照 6. 選中生成的第二個快照報告,并將視圖由"Summary"切換到"Comparison"對比模式,在\[class filter\]過濾輸入框中輸入關鍵字:\*\*Detached\*\* !\[dom\](./images/dom2.png) 從分析結果圖可知,導致整個pre元素和6個文本節點無法別回收的原因是:代碼中存在全局變量`wrapper`對pre元素的引用。知道了產生的問題原因,便可對癥下藥了。對代碼做如下就修改: ```js // 因為要多次用到pre.wrapper、div.container、input.remove、input.add節點,將其緩存到本地變量中, var wrapper = document.querySelector('.wrapper'); var container = document.querySelector('.container'); var removeBtn = document.querySelector('.remove'); var addBtn = document.querySelector('.add'); var counter = 0; var once = true; // 方法 var hide = function(target){ target.style.display = 'none'; } var show = function(target){ target.style.display = 'inline-block'; } // 回調函數 var removeCallback = function(){ removeBtn.removeEventListener('click', removeCallback, false); addBtn.removeEventListener('click', addCallback, false); hide(addBtn); hide(removeBtn); container.removeChild(wrapper); wrapper = null;//在執行刪除操作時,將wrapper對pre節點的引用釋放掉 } var addCallback = function(){ wrapper.appendChild(document.createTextNode('\\t' + ++counter + ':a new line text\\n')); // 顯示刪除操作按鈕 if(once){ show(removeBtn); once = false; } } // 綁定事件 removeBtn.addEventListener('click', removeCallback, false); addBtn.addEventListener('click', addCallback, false); ``` 在執行刪除操作時,將wrapper對pre節點的引用釋放掉,即在刪除邏輯中增加`wrapper = null;`語句。再次在Devtools–>Performance中重復上述操作: !\[dom\](./images/dom3.png) \### 小試牛刀------>\[demos/dom\_practice.html\](./demos/dom\_practice.html) 再來看看網上的一個實例,代碼如下: ```html Practice var refA = document.getElementById('refA'); var refB = document.getElementById('refB'); document.body.removeChild(refA); // #refA不能GC回收,因為存在變量refA對它的引用。將其對#refA引用釋放,但還是無法回收#refA。 refA = null; // 還存在變量refB對#refA的間接引用(refB引用了#refB,而#refB屬于#refA)。將變量refB對#refB的引用釋放,#refA就可以被GC回收。 refB = null; ``` 整個過程如下圖所演示: !\[\](./images/memory.gif) 有興趣的同學可以使用Chrome的Devtools工具,驗證一下分析結果,實踐很重要~~~:high\_brightness: \## timers 在JavaScript常用`setInterval()`來實現一些動畫效果。當然也可以使用鏈式`setTimeout()`調用模式來實現: ```js setTimeout(function() { // do something. . . . setTimeout(arguments.callee, interval); }, interval); ``` 如果在不需要`setInterval()`時,沒有通過`clearInterval()`方法移除,那么`setInterval()`會不停地調用函數,直到調用`clearInterval()`或窗口關閉。如果鏈式`setTimeout()`調用模式沒有給出終止邏輯,也會一直運行下去。因此再不需要重復定時器時,確保對定時器進行清除,避免占用系統資源。另外,在使用`setInterval()`和`setTimeout()`來實現動畫時,無法確保定時器按照指定的時間間隔來執行動畫。為了能在JavaScript中創建出平滑流暢的動畫,瀏覽器為JavaScript動畫添加了一個新API-requestAnimationFrame()。\[關于setInterval、setTimeout與requestAnimationFrame實現動畫上的區別?猛擊??\](https://github.com/zhansingsong/js-leakage-patterns/blob/master/requestAnimationFrame/requestAnimationFrame.md) \### 實例------>\[demos/timers.html\](./demos/timers.html) 如下通過`setInterval()`實現一個clock的小實例,不過代碼存在問題的,有興趣的同學可以先嘗試找一下問題的所在~~~~~?? 操作: \- 單擊【start】按鈕開始clock,同時web開發控制臺會打印實時信息 \- 單擊【stop】按鈕停止clock,同時web開發控制臺會輸出停止信息 ```html setInterval var counter = 0; var clock = { start: function () { setInterval(this.step.bind(null, ++counter), 1000); }, step: function (flag) { var date = new Date(); var h = date.getHours(); var m = date.getMinutes(); var s = date.getSeconds(); console.log("%d-----> %d:%d:%d", flag, h, m, s); } } document.querySelector('.start').addEventListener('click', clock.start.bind(clock), false); document.querySelector('.stop').addEventListener('click', function () { console.log('----> stop <----'); clock = null; }, false); ``` 上述代碼存在兩個問題: 1. 如果不斷的單擊【start】按鈕,會斷生成新的clock。 2. 單擊【stop】按鈕不能停止clock。 輸出結果: !\[\](./images/setinterval.png) 針對暴露出的問題,對代碼做如下修改: ```js var counter = 0; var clock = { timer: null, start: function () { // 解決第一個問題 if (this.timer) { clearInterval(this.timer); } this.timer = setInterval(this.step.bind(null, ++counter), 1000); }, step: function (flag) { var date = new Date(); var h = date.getHours(); var m = date.getMinutes(); var s = date.getSeconds(); console.log("%d-----> %d:%d:%d", flag, h, m, s); }, // 解決第二個問題 destroy: function () { console.log('----> stop <----'); clearInterval(this.timer); node = null; counter = void(0); } } document.querySelector('.start').addEventListener('click', clock.start.bind(clock), false); document.querySelector('.stop').addEventListener('click', clock.destroy.bind(clock), false); ``` \## EventListener 做移動開發時,需要對不同設備尺寸做適配。如在開發組件時,有時需要考慮處理橫豎屏適配問題。一般做法,在橫豎屏發生變化時,需要將組件銷毀后再重新生成。而在組件中會對其進行相關事件綁定,如果在銷毀組件時,沒有將組件的事件解綁,在橫豎屏發生變化時,就會不斷地對組件進行事件綁定。這樣會導致一些異常,甚至可能會導致頁面崩掉。 \### 實例------>\[demos/callbacks.html\](./demos/callbacks.html) ```html callbacks var container = document.querySelector('.container'); var counter = 0; var createHtml = function (n, counter) { var template = `${(new Array(n)).join(`${counter}: this is a new data `)}` container.innerHTML = template; } var resizeCallback = function (init) { createHtml(10, ++counter); // 事件委托 container.addEventListener('click', function (event){ var target = event.target; if(target.tagName === 'INPUT'){ container.removeChild(target.parentElement) } }, false); } window.addEventListener('resize', resizeCallback, false); resizeCallback(true); ``` 頁面是存在問題的,這里結合Devtools–>Performance分析一下問題所在,操作步驟如下: \*\*:warning:注:最好在隱藏窗口中進行分析工作,避免瀏覽器插件影響分析結果\*\* 1. 開啟Performance項的記錄 2. 執行一次CG,創建基準參考線 3. 對窗口大小進行調整 4. 執行一次CG 5. 停止記錄 !\[callbacks\](./images/callback.png) 如分析結果所示,在窗口大小變化時,會不斷地對`container`添加代理事件。 同一個元素節點注冊了多個相同的EventListener,那么重復的實例會被拋棄。這么做不會讓得EventListener被重復調用,也不需要用removeEventListener手動清除多余的EventListener,因為重復的都被自動拋棄了。而這條規則只是針對于命名函數。\[對于匿名函數,瀏覽器會將其看做不同的EventListener\](https://triangle717.wordpress.com/2015/12/14/js-avoid-duplicate-listeners/),所以只要將匿名的EventListener,命名一下就可以解決問題: ```js var container = document.querySelector('.container'); var counter = 0; var createHtml = function (n, counter) { var template = `${(new Array(n)).join(`${counter}: this is a new data `)}` container.innerHTML = template; } // var clickCallback = function (event) { var target = event.target; if (target.tagName === 'INPUT') { container.removeChild(target.parentElement) } } var resizeCallback = function (init) { createHtml(10, ++counter); // 事件委托 container.addEventListener('click', clickCallback, false); } window.addEventListener('resize', resizeCallback, false); resizeCallback(true); ``` 在Devtools–>Performance中再重復上述操作,分析結果如下: !\[callback\](./images/callback1.png) 在開發中,開發者很少關注事件解綁,因為瀏覽器已經為我們處理得很好了。不過在使用第三方庫時,需要特別注意,因為一般第三方庫都實現了自己的事件綁定,如果在使用過程中,在需要銷毀事件綁定時,沒有調用所解綁方法,就可能造成事件綁定數量的不斷增加。如下鏈接是我在項目中使用jquery,遇見到類似問題:\[jQuery中忘記解綁注冊的事件,造成內存泄露?猛擊??\](https://github.com/zhansingsong/js-leakage-patterns/blob/master/%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E4%B9%8BListeners/%E5%86%85%E5%AD%98%E6%B3%84%E9%9C%B2%E4%B9%8BListeners.md) \## 總結 本文主要介紹了幾種常見的內存泄露。在開發過程,需要我們特別留意一下本文所涉及到的幾種內存泄露問題。因為這些隨時可能發生在我們日常開發中,如果我們對它們不了解是很難發現它們的存在。可能在它們將問題影響程度放大時,才會引起我們的關注。不過那時可能就晚了,因為產品可能已經上線,接著就會嚴重影響產品的質量和用戶體驗,甚至可能讓我們承受大量用戶流失的損失。作為開發的我們必須把好這個關,讓我們開發的產品帶給用戶最好的體驗。 \## 參考文章: \- \[An interesting kind of JavaScript memory leak\](https://blog.meteor.com/an-interesting-kind-of-javascript-memory-leak-8b47d2e7f156) \- \[Memory Leaks in Microsoft Internet Explorer\](http://isaacschlueter.com/2006/10/msie-memory-leaks/trackback/index.html) \- \[Memory leak when logging complex objects\](https://stackoverflow.com/questions/12996129/memory-leak-when-logging-complex-objects)
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看