Blackbing Playground

ie6 Memory Leak的解決方式

前端程式越搞越複雜,接踵而至的問題越來越多,雖然瀏覽器的效能不斷地提昇,但是只要IE6沒被淘汰的一天,網頁程式設計師永遠要替IE6補洞,Memory Leak是一個非常難搞得議題。關於微軟的官方說法可以參考Understanding and Solving Internet Explorer Leak Patterns,裡頭有詳細說明IE6會如何發生memory leak的情況。發生memory leak的徵狀就是:記憶體不斷飆上去,就算reload頁面,記憶體也不會被釋放,除非關掉瀏覽器。


在此不介紹為何為引發memory leak,因為我覺得這是瀏覽器應該要處理的問題,要拿出來講只是因為IE6沒辦法處理好罷了,雖然其他瀏覽器也並不是沒有這個問題,但是至少問題都沒有這麼嚴重,因此只需要針對IE6來處理即可。以下是我整理出幾種比較好的處理方式:



  1. Douglas Crockford針對memory leak的議題發表一篇文章:JScript Memory Leaks提出一個purge的function,概念非常簡單,就是遍歷元素底下的所有節點,將所有的function設為null(這屬於closure的循環參照索引發的memory leak)。一般來說可以在unload時或者你要removeNode之前來做這個動作,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    function purge(d) {
    var a = d.attributes, i, l, n;
    if (a) {
    l = a.length;
    for (i = 0; i < l; i += 1) {
    n = a[i].name;
    if (typeof d[n] === 'function') {
    d[n] = null;
    }
    }
    }
    a = d.childNodes;
    if (a) {
    l = a.length;
    for (i = 0; i < l; i += 1) {
    purge(d.childNodes[i]);
    }
    }
    }
    window.onunload = function(){
    purge(document.body);
    }



  2. Event Cache :若你的專案沒用到一些framework時,可能會用到一些自訂的addEvent方法,這時可以透過EventCache.add來將事件加入cache,然後在unload時做EventCache.flush。這樣可以避免在unload時循環整個body,雖然降低了memory leak,但卻吃掉cpu的效能。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    var EventCache = function () {
    var listEvents = [];
    return {
    listEvents: listEvents,
    add: function (node, sEventName, fHandler, bCapture) {
    listEvents.push(arguments);
    },
    flush: function () {
    var i, item;
    for (i = listEvents.length - 1; i & gt; = 0; i = i - 1) {
    item = listEvents[i];
    if (item[0].removeEventListener) {
    item[0].removeEventListener(item[1], item[2], item[3]);
    };
    /* From this point on we need the event names to be prefixed with 'on" */
    if (item[1].substring(0, 2) != & quot; on & quot;) {
    item[1] = & quot;
    on & quot; + item[1];
    };
    if (item[0].detachEvent) {
    item[0].detachEvent(item[1], item[2]);
    };
    item[0][item[1]] = null;
    };
    }
    };
    }();



  3. 利用try…finally來清除不被引用的參考:這常會發生在我們利用function來creat Element時,忽略了不再被引用的參考,然後直接return,在IE6中會引發memory leak,這篇文章透過try,finally來清除不被引用的參考,例如:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    function createButton() {
    var obj = document.createElement("button");
    obj.innerHTML = "click me";
    obj.onclick = function() {
    //handle onclick
    }
    obj.onmouseover = function() {
    //handle onmouseover
    }
    //this helps to fix the memory leak issue
    try {
    return obj;
    } finally {
    obj = null;
    }
    }



  4. innerHTML所引發的memory leak:這個議題最為常見,innerHTML不被W3C列為標準,但因為他比起操作DOM元素容易的多了,因此廣泛的被使用,但是之前有提到,最好是在removeNode時做一次檢查,將node裡頭的function全部設為null,以避免closure所引發的memory leak,但是innerHTML實在太方便了,讓所有programer將這個問題拋在腦後,因為我隨時可以設定某個元素的innerHTML,哪怕他裡頭又肥又大,一句innerHTML就可以改變DOM元素。文章裡頭的例子用YUI來做示範,不過事實上,他還是用了Douglas了perge來清除循環引用。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    YAHOO.util.Dom.setInnerHTML = function (el, html) {
    el = YAHOO.util.Dom.get(el);
    if (!el || typeof html !== 'string') {
    return null;
    }
    // Break circular references.
    (function (o) {
    var a = o.attributes, i, l, n, c;
    if (a) {
    l = a.length;
    for (i = 0; i < l; i += 1) {
    n = a[i].name;
    if (typeof o[n] === 'function') {
    o[n] = null;
    }
    }
    }
    a = o.childNodes;
    if (a) {
    l = a.length;
    for (i = 0; i < l; i += 1) {
    c = o.childNodes[i];
    // Purge child nodes.
    arguments.callee(c);
    // Removes all listeners attached to the element via YUI's addListener.
    YAHOO.util.Event.purgeElement(c);
    }
    }
    })(el);
    // Remove scripts from HTML string, and set innerHTML property
    el.innerHTML = html.replace(/<script[^>]*>[\S\s]*?<\/script[^>]*>/ig, "");
    // Return a reference to the first child
    return el.firstChild;
    };




大概是這樣了,memory leak實在是個很惱人的議題,不過有責任感的前端工程是還是要將之視為己任的,畢竟項目越做越大,然而記憶體越吃越兇,這是很可怕的一件事。