1. 程式人生 > >如何監聽頁面就緒(早於window.onload)

如何監聽頁面就緒(早於window.onload)

原文地址:http://www.javascriptkit.com/dhtmltutors/domready.shtml

在DOM樹就緒時就啟動頁面執行JavaScript(而不是使用window.onload)

如下的例子展示了一個js語法中最常用的檢查頁面是否載入完畢的事件:

window.onload=function(){
    walkmydog();
}

這種呼叫方式幾乎是我們監聽web文件是否載入完畢使用最頻繁的寫法。因為通過將頁面js入口控制在window.onload之內,你就可以放心的在任何地方引用js檔案而不用擔心js執行的時候頁面還沒有準備好。對於這個方法有一點需要注意的就是,這個方法會在整個頁面完全載入就緒之後才出發,而不管頁面上載入的內容是不是我們真正需要的。因此,也就導致了window.onload被眾人詬病的缺點——window.onload需要等待整個頁面的字面內容全部載入,這意味著會等待形如圖片、iframe框架都載入完畢並呈現後,window.onload方法才執行,很多內容往往不是我們必須等待的。這就是為什麼Dean Edwards提供了一種替代方案來偵測DOM是否就緒(即指檢測DOM樹完成初始化),而不是整個文件和所有物件都就緒。舉個典型的例子:向DOM樹插入/刪除節點,這個操作實際上可以被完成的更早,即DOM樹被載入完成而不是整個HTML文件被載入並渲染好(放在window.onload中往往導致了效能損失)。根據這樣的指導意見,我或許能提煉一下Dean Edwards對於在DOM樹就緒後就儘早執行js的技術。

如下是一個我們將要討論的技術的一個簡單示例

·Firefox和Opera9+中偵聽DOM就緒

在Firefox和Mozilla,檢查DOM樹就緒時執行js是很簡單的,我們可以在呼叫document.addEventListener()的時候監聽一個特定事件"DOMContentLoaded":

//FF&Opera
if (document.addEventListener)
    document.addEventListener("DOMContentLoaded", walkmydog, false)

在這種情況下walkmydog()方法將會在文件的DOM樹初始化後就被執行,他將會在window.onload方法之前多快被執行取決於頁面所包含的內容(包含越多的圖片、iframe框架,時間差距就會越大)。

·Internet Explorer偵聽DOM就緒

在IE(包括IE7)中,沒有一個原生的方法來偵測DOM樹就緒。Dean Edwards提供了一種圓滑(繞圈子)的解決方案,這種方案依賴於頁面內js標籤的“延遲”(defer屬性)特性。簡而言之這個特性使得IE可以“推遲”載入外部js檔案直到DOM樹被載入完畢,這是一種在IE瀏覽器內監聽DOM樹就緒最優的解決方案:

//IE
if (document.all && !window.opera){ //Crude test for IE
    //Define a "blank" external JavaScript tag
    document.write('<script type="text/javascript" id="contentloadtag" defer="defer" src="javascript:void(0)"><\/script>')
    var contentloadtag=document.getElementById("contentloadtag")
    contentloadtag.onreadystatechange=function(){
        if (this.readyState=="complete")
            walkmydog()
    }
}

正如你所見,這項技術以定義一個帶有”defer”屬性的空的外部js標籤為核心。在IE中,大部分元素支援"onreadystatechange"事件,這個事件會在元素載入過程中的每一個階段觸發,並最終以”complete”結束。因此通過監聽一個帶有”defer”屬性的空的外部js標籤的onreadystatechange事件,我們就可以準確的在DOM樹就緒的時候執行js方法。

·合併處理,以及一個回退策略

你剛剛見識了兩個分散的技術來偵測DOM樹就緒。現在讓我們把分散的兩個方法合併在一起,更重要的,下面的例子為不支援前述兩種方法的瀏覽器實現一個回退機制,這個回退策略就是使用window.onload:

//fall back
var alreadyrunflag=0 //flag to indicate whether target function has already been run

if (document.addEventListener)
    document.addEventListener("DOMContentLoaded", function(){alreadyrunflag=1; walkmydog()}, false)
else if (document.all && !window.opera){
    document.write('<script type="text/javascript" id="contentloadtag" defer="defer" src="javascript:void(0)"><\/script>')
    var contentloadtag=document.getElementById("contentloadtag")
    contentloadtag.onreadystatechange=function(){
        if (this.readyState=="complete"){
            alreadyrunflag=1
            walkmydog()
        }
    }
}

window.onload=function(){
    setTimeout("if (!alreadyrunflag) walkmydog()", 0)
}

注意第5,17,18,19行的程式碼-這就是我們在不支援偵測DOM就緒方法的瀏覽器中實現的回退機制,我們使用了window.onload事件來代替。有了這樣的處理,現代瀏覽器會在DOM樹就緒後立即執行,老舊瀏覽器會在window.onload方法觸發時執行js程式碼,所有瀏覽器都會在頁面就緒後執行。為了使這個方案能夠正常工作,有兩點我想提醒各位:

(1)上述程式碼定義了一個全域性變數"alreadyrunflag",來使執行回退方案的瀏覽器知道是否需要通過window.onload來執行js方法。如果標記表明"true",就表明js方法在早先時候DOM樹就緒的階段已經被執行過,window.onload方法觸發時就不會再重複執行。

(2)針對上述程式碼,在IE中啟動非常簡單的頁面時,可能會出現window.onload和DOM樹就緒(外部js標籤readyState變成"complete" )同時觸發的情況。在這種特殊情況下,在我們的回退方案執行的時候alreadyrunflag標記也許不會變成true,IE瀏覽器也就不知道js方法已經被執行過,可能導致繫結的js方法將會執行兩編。這個問題可以通過在window.onload中編寫一個嵌入式的setTimeout()來解決,從而確保相關程式碼會被緊跟在當前程式碼之後被執行。

通過這種方式你就擁有了一個能使js儘早在DOM樹載入完畢後就立即執行的方法,而不會在頁面尚未準備完畢時就提前執行。使用相同的技巧,下面的例子展示瞭如何執行兩個方法,並且後者會傳入一個引數:

//two functions
var alreadyrunflag=0 //flag to indicate whether target function has already been run

if (document.addEventListener)
    document.addEventListener("DOMContentLoaded", function(){alreadyrunflag=1; walkmydog(); feedcat('grapes')}, false)
else if (document.all && !window.opera){
    document.write('<script type="text/javascript" id="contentloadtag" defer="defer" src="javascript:void(0)"><\/script>')
    var contentloadtag=document.getElementById("contentloadtag")
    contentloadtag.onreadystatechange=function(){
        if (this.readyState=="complete"){
            alreadyrunflag=1
            walkmydog()
            feedcat('grapes')
        }
    }
}

window.onload=function(){
    setTimeout("if (!alreadyrunflag){walkmydog(); feedcat('grapes')}", 0)
}

·總結,以及在Safari中偵測DOM就緒

使用上述方法來使js在DOM樹就緒時就立即執行而不是等到window.onload觸發才執行的優點體現在一些比較複雜的頁面上或者iframe標籤裡。最終你會從這些額外的程式碼和工作中獲得足夠的收益。在我總結這份文章之前,房間裡還有一頭大象一直被我忽視到現在(即指沒有提供Safari的相容方案)。在Safari中實際上也是可以偵測DOM就緒事件的,Dean Edward提到John Resig(jQuery作者)為這種瀏覽器提供了一種方案:

//Safari
if(/Safari/i.test(navigator.userAgent)){ //Test for Safari
    var _timer=setInterval(function(){
            if(/loaded|complete/.test(document.readyState)){
                clearInterval(_timer)
                walkmydog() // call target function
            }}
        , 10)
}

這是一個DOM樹就緒檢查器!在Safari中,實際上,你需要做的就是在一個快速的輪詢定時器中手工的去探查document.readyState屬性是否變成了"loaded" 或"completed"狀態(通常前者會早於後者,但是為了保險起見,我們將一起監聽兩個狀態)。這就是DOM樹就緒並執行你的js方法的時刻。就我個人而言,我並不熱衷於實現這個方法,因為這種實現方式跨越了技術底線:成本高於了收益(譯者注:正則表示式以及輪詢定時器的使用將會極大的損耗效能)。不過最終我們都有權利選擇我們使用的技術方案。