讓插入到innerHTML中的script跑起來(轉http://blog.csdn.net/lee576/archive/2008/11/20/3341146.aspx)

在做 ajax 編程時,我們常常需要將 xmlhttp 獲取到的頁面內容通過 innerHTML 來賦給某個容器(比如 div、span 或者 td 等),但是這裏存在一個問題,就是我們將要賦給 innerHTML 的頁面內容如果包含有腳本程序,這些腳本程序不管是外部腳本,還是內部腳本,可能(1)都不會被執行。這個問題在某些時候微不足道,甚至可以忽略,但有些時候,這個問題就非常嚴重,它很可能讓我們的程序得不到預期的結果。因此我們需要解決這個問題。

如果你讀過 MSDN,你會發現並非所有插入到 innerHTML 中的腳本都不能執行,如果這段腳本的 script 標籤中包含了 defer 屬性,IE 會正確的執行這些腳本程序。但不幸的是,Moziila/Firefox 和 Opera 可不喫這一套,不管 script 標籤有沒有設置 defer 屬性,這些瀏覽器都不會向 IE 那樣去執行插入到 innerHTML 中的腳本。

但不管腳本是否被執行了,有一點我們可以肯定,那就是這些腳本確實被插入到了 innerHTML 中,如果不相信,你可以 alert 一下看看。但如果你真的 alert 了,你也可能會發現有一種例外情況存在,那就是如果腳本在 innerHTML 內容開頭的話,那麼 IE 瀏覽器將會忽略掉這段腳本,而 Moziila/Firefox 和 Opera 卻不會。

好了,問題分析的差不多了,我們來看看如何解決吧。

解決的思路其實很簡單,那就是將插入到 innerHTML 中的所有腳本取出來,然後一一執行。不過我們先要解決上面的兩個問題。

先來看第一個問題,如何避免在 IE 中重複執行 innerHTML 中帶有 defer 屬性的腳本。這個很容易,只需要先確定瀏覽器是否是 IE,然後再檢測將要執行的腳本是否帶有 defer 屬性即可。需要注意的是,在判斷 IE 瀏覽器時,我們需要避免被 opera 的瀏覽器識別欺騙。這一點我們在後面的代碼中將會看到它是如何做的。

接下來,看 IE 忽略 innerHTML 開頭腳本的問題,這個也很容易解決。只需要在要插入到 innerHTML 中的內容的開頭附加一段不是腳本的內容,就可以了。但不要試圖附加一個空內容的標籤,或者空格、回車、換行等,這將不起作用,開頭的腳本仍然會被忽略。也不要試圖附加  ,雖然這可以讓開頭的腳本不再被忽略,但這個   仍然會影響原有內容的顯示,雖然你可能覺得不明顯,但是對於挑剔的用戶來說,這可能是無法容忍的。因此,爲了讓附加的內容既可以起到避免開頭腳本被忽略的功能,又不會造成不良影響,我們將附加這麼一段內容:

<span style="display: none">hack ie</span>
雖然上面這段內容有一定的長度,但是它並不會顯示,而且這個插入的標籤沒有 id 也沒有 name,所以也不會跟原來內容中的某些標籤的 id 或者 name 產生衝突。不過這裏有一點要注意,這裏也要判斷是否是 IE,然後再決定加不加這段內容,因爲其他某些瀏覽器可能不支持 display: none 這個 CSS 修飾(例如 Opera Mini),如果加上這段代碼會影響最終的顯示效果。

下面我們來看看如何取出腳本並執行。

取出腳本很容易,只需要用 innerHTML 所在對象的 getElementsByTagName 方法就可以了,這個方法對幾乎所有的容器標籤都管用。取出腳本以後,我們要一一判斷它們是外部腳本還是內部腳本。

先來看外部腳本,如果是外部腳本,我們選擇了這樣一種方法,即先創建這個外部腳本的一個副本對象,並設置它的 defer 屬性爲 true(這一點是爲了讓 IE 瀏覽器能正確執行),然後用 appendChild 方法將這個副本對象插入到 head 中。這裏你可能會問,爲什麼不是插入到 innerHTML 所在的對象中呢?插入到 innerHTML 所在的對象中不是更好嗎?如果你試一下就會知道,如果插入到 innerHTML 所在的對象中,在 IE 瀏覽器中沒有問題,但是在 Mozilla/Firefox 和 Opera 瀏覽器中會有一些問題。問題是如果在 Firefox 上這樣做,瀏覽器會停止響應(這是在 Firefox 1.5 上的測試結果,其他版本是否有此問題,尚不得知),而在 Opera 上,腳本會莫名其妙的執行兩次(這是在 Opera 8.5 上的測試結果,其它版本的 Opera 是否由此問題,也尚不得知)。爲了避免這些問題,所以我選擇了插入到 head 中。

再來看內部腳本,內部腳本的內容我們可以直接用腳本對象的 text 屬性來獲取,這裏我們使用腳本對象的 text 屬性而不是 innerHTML 屬性,是因爲在 Opera 瀏覽器中,腳本對象的 innerHTML 屬性是空的,只有用 text 屬性才能獲取到腳本內容。執行內部腳本直接用 eval 即可。但是腳本可能會被包含在 HTML 的註釋標籤中,因此我們需要先將註釋標籤去掉,不然在 IE 中會出錯。

上面的分析看上去很完美了,但是實際上還是有問題,一個是 document.write 和 document.writeln 的問題,這個問題在 Blueidea 上,bound0 給出了一個思路,就是替換掉默認的 document.write 和 document.writeln 方法,不過他用的是字符串替換,因此只對內部腳本有效,對外部腳本就沒辦法了,因此我想了個更通用的辦法,就是直接把 document.write 和 document.writeln 重新定義,這樣不管內部腳本還是外部腳本執行的就都是我們我們自己定義的 document.write 和 document.writeln 了。不過也有副作用,就是這兩個函數在當前頁面中就不能再像原來一樣使用了,不過這兩個函數在頁面加載完之後一般是不會再用到了,因此這裏重新定義它們所帶來的副作用影響很小。但是還有個問題是,儘管這樣,我們仍然無法保證 document.write 或 document.writeln 輸出的內容會顯示在最合適的位置,它只是把內容附加到了我們放置內容的容器中。

另一個問題是 eval 引起的問題,一個是 Blueidea 上的 hutia 說的作用域的問題,另一個問題是如果用 eval 執行的內部腳本的話,內部腳本會在外部腳本加載完之前就開始執行了。要解決這個兩個問題可以採用 window.setTimeout 這個函數,讓每個腳本都延時一段後再執行,外部腳本延時時間可以設的較長,以保證其能夠完全加載,而內部腳本則可以設置爲很短,因爲一個腳本執行的時間通常是很短的,這樣既可以保證不會改變作用域,又可以基本保證腳本執行順序不會改變了(這種方法對於保證執行順序上也不一定會 100% 有效,如果網絡非常繁忙,外部腳本可能在設置的時間內加載不完,但至少比直接用 eval 的時候好多了)。

如果按照前面的方式實現,對於大多數腳本來說可以正常執行了。但是如果 script 中帶有 defer 屬性,IE 會自己運行那段代碼(前面提過了),因此它會打亂執行的順序。另外 document.write 和 document.writeln 寫入的代碼都回被添加到最後面,而不是腳本所在的位置上,因此這也是個問題。

爲了解決這兩個問題,我們需要對前面的解決方法作一些改變。首先我們不能先把內容賦值給 innerHTML,然後再通過它取腳本了,我們需要直接對內容分析來取出腳本。另外,腳本以外的 HTML 部分也不能直接賦值給 innerHTML,需要在腳本執行以後,將原有的 HTML 內容和 document.write/writeln 寫的內容按照順序合併到一起再賦值給 innerHTML,這裏要注意,我們不能一部分一部分的將這些內容連接到 innerHTML 後面,因爲其中可能有半個標籤的內容,這種情況下,瀏覽器很容易發生錯誤。而且你會看到頁面反覆刷新的情況出現。而如果先放入緩衝區,最後一次賦給 innerHTML,就不會出現這種問題了。

另外放入緩衝區的好處是,當腳本執行完後,可以檢查緩衝區中是否還有新的腳本,如果有,再遞歸執行,這樣就可以解決 document.write 和 document.writeln 寫的腳本也可以執行的問題了。

2006-6-4 更新:

修正了插入到 innerHTML 中的腳本無法獲取插入到 innerHTML 中對象的問題。(感謝網友 DE 的提醒)。

增加了對同一容器中內容設置的共享鎖,使得連續設置同一個容器內的時,不會再發生衝突。(感謝新加坡網友 Jason Li 的提醒)。

2006-5-29 更新:

增加了使用外部腳本緩存功能,提高了第二次加載相同外部腳本的速度。

2006-5-23 更新:

在熱心的使用者 johnZEN 的提醒下,增加了共享鎖,使得同時設置多個容器內的內容時,不會再發生衝突。

在網友 udbjatwfn 的提醒下,修正了 IE 中存在的內部腳本執行作用域錯誤的問題。

下面是最後本人的實現代碼:

JavaScript代碼
 
/* innerhtml.js  
 * Copyright Ma Bingyao <[email protected]>  
 * Version: 1.9  
 * LastModified: 2006-06-04  
 * This library is free.  You can redistribute it and/or modify it.  
 * http://www.coolcode.cn/?p=117  
 */  
  
var global_html_pool = [];   
var global_script_pool = [];   
var global_script_src_pool = [];   
var global_lock_pool = [];   
var innerhtml_lock = null;   
var document_buffer = "";   
  
function set_innerHTML(obj_id, html, time) {   
    if (innerhtml_lock == null) {   
        innerhtml_lock = obj_id;   
    }   
    else if (typeof(time) == "undefined") {   
        global_lock_pool[obj_id + "_html"] = html;   
        window.setTimeout("set_innerHTML('" + obj_id + "', global_lock_pool['" + obj_id + "_html']);", 10);   
        return;   
    }   
    else if (innerhtml_lock != obj_id) {   
        global_lock_pool[obj_id + "_html"] = html;   
        window.setTimeout("set_innerHTML('" + obj_id + "', global_lock_pool['" + obj_id + "_html'], " + time + ");", 10);   
        return;   
    }   
  
    function get_script_id() {   
        return "script_" + (new Date()).getTime().toString(36)   
          + Math.floor(Math.random() * 100000000).toString(36);   
    }   
  
    document_buffer = "";   
  
    document.write = function (str) {   
        document_buffer += str;   
    }   
    document.writeln = function (str) {   
        document_buffer += str + "/n";   
    }   
  
    global_html_pool = [];   
  
    var scripts = [];   
    html = html.split(/<//script>/i);   
    for (var i = 0; i < html.length; i++) {   
        global_html_pool[i] = html[i].replace(/<script[/s/S]*$/ig, "");   
        scripts[i] = {text: '', src: '' };   
        scripts[i].text = html[i].substr(global_html_pool[i].length);   
        scripts[i].src = scripts[i].text.substr(0, scripts[i].text.indexOf('>') + 1);   
        scripts[i].src = scripts[i].src.match(/src/s*=/s*(/"([^/"]*)/"|/'([^/']*)/'|([^/s]*)[/s>])/i);   
        if (scripts[i].src) {   
            if (scripts[i].src[2]) {   
                scripts[i].src = scripts[i].src[2];   
            }   
            else if (scripts[i].src[3]) {   
                scripts[i].src = scripts[i].src[3];   
            }   
            else if (scripts[i].src[4]) {   
                scripts[i].src = scripts[i].src[4];   
            }   
            else {   
                scripts[i].src = "";   
            }   
            scripts[i].text = "";   
        }   
        else {   
            scripts[i].src = "";   
            scripts[i].text = scripts[i].text.substr(scripts[i].text.indexOf('>') + 1);   
            scripts[i].text = scripts[i].text.replace(/^/s*</!--/s*/g, "");   
        }   
    }   
  
    var s;   
    if (typeof(time) == "undefined") {   
        s = 0;   
    }   
    else {   
        s = time;   
    }   
  
    var script, add_script, remove_script;   
  
    for (var i = 0; i < scripts.length; i++) {   
        var add_html = "document_buffer += global_html_pool[" + i + "];/n";   
        add_html += "document.getElementById('" + obj_id + "').innerHTML = document_buffer;/n";   
        script = document.createElement("script");   
        if (scripts[i].src) {   
            script.src = scripts[i].src;   
            if (typeof(global_script_src_pool[script.src]) == "undefined") {   
                global_script_src_pool[script.src] = true;   
                s += 2000;   
            }   
            else {   
                s += 10;   
            }   
        }   
        else {   
            script.text = scripts[i].text;   
            s += 10;   
        }   
        script.defer = true;   
        script.type =  "text/javascript";   
        script.id = get_script_id();   
        global_script_pool[script.id] = script;   
        add_script = add_html;   
        add_script += "document.getElementsByTagName('head').item(0)";   
        add_script += ".appendChild(global_script_pool['" + script.id + "']);/n";   
        window.setTimeout(add_script, s);   
        remove_script = "document.getElementsByTagName('head').item(0)";   
        remove_script += ".removeChild(document.getElementById('" + script.id + "'));/n";   
        remove_script += "delete global_script_pool['" + script.id + "'];/n";   
        window.setTimeout(remove_script, s + 10000);   
    }   
  
    var end_script = "if (document_buffer.match(/<///script>/i)) {/n";   
    end_script += "set_innerHTML('" + obj_id + "', document_buffer, " + s + ");/n";   
    end_script += "}/n";   
    end_script += "else {/n";   
    end_script += "document.getElementById('" + obj_id + "').innerHTML = document_buffer;/n";   
    end_script += "innerhtml_lock = null;/n";   
    end_script += "}";   
    window.setTimeout(end_script, s);   
}  
演示程序地址:Demo

不過感覺本人的實現過於笨拙, Ajax Wing: 跨瀏覽器的設置innerHTML方法 的實現更精闢一些,效果也更好,本人實在是非常的佩服!不過這個實現有一點問題就是腳本中的 document.write 和 document.writeln 縮寫的內容位置是不對的。

下面是他的實現的演示程序地址:Demo2

 

本文來自CSDN博客,轉載請標明出處:http://blog.csdn.net/lee576/archive/2008/11/20/3341146.aspx

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章