我的博客已經寫過好幾篇如何實現domReady的文章,最近做培訓,面向新手們,需要徹徹底底向他們說明這個東西,於是就有了這篇文章。
我們經常看人們用
document.getElementById("xxx").style.left = "80px"
報錯,說找不到元素.但明明頁面上有包含xxx這個ID的元素的
這其實就是分不清HTML標簽與DOM節點之故了。
HTML是一種標記語言, 告訴我們這頁面有什么內容。 但行為交互是需要通過DOM操作實現, 不要以后那兩個尖括號的內容就是一個DOM
HTML標簽要通過瀏覽器解析才會變成DOM節點。
當我們向地址欄傳入一個URL, 開始加載頁面到我們看到內容,這期間就有一個DOM節點構建的過程。
節點們是以樹的形式組織的, 當頁面上所有HTML都轉換為節點, 這就叫做DOM樹建完, 簡稱之domReady。
HTML轉換DOM是一個非常復雜的過程,想深入的同學可以看一下這個地址
我們簡單說一下, 瀏覽器是從上到下, 從左到右,一個個字符串讀入, 大致可以認為兩個同名的開標簽與閉標簽就是一個DOM(有的是沒有閉簽),這時就忽略掉它的兩個標簽間的內容。頁面上有許多標簽, 但標簽會生成同樣多的DOM,因為有的標簽下只允許存在特定的子標簽, 比如tr下面一定是td,th, select下面一定是opgroup,option,而option下面,就算你寫了<span></span>,它都會忽略掉,option下面只存在文本,這就是我們需要自定義下拉框的緣故. 我們說過, 這順序是從上到下, 有的元素很簡單,會構建得很快,但標簽存在src, href屬性,它會引用外部資源,這就要區別對待了.比如說, script標簽,它一定會等src指定的腳本文件加載下來,然后全部執行了里面的腳本,才會分析下一個標簽.這種現象叫做堵塞.
堵塞是一種非常致命的現象,因為瀏覽器渲染引擎是單線程的,如果頭部腳本過多過大會導致白屏,影響用戶體驗,因此雅虎的20軍規就有一條提到 ,將所有script標簽放到body之后.
此外, style標簽與link標簽,它們在加載樣式文件時是不會堵塞,但它們一旦異步加載好,就立即開始渲染已經構建好的元素節點們, 這可能會引起reflow, 這也影響速度.
另一個影響DOM樹構建的因此是iframe,它也會加載資源, 雖然不會堵塞DOM構建,但它由於是發出HTTP請求,而HTTP請求是有限,它會與父標簽的其他需要加載外部資源的標簽產生競爭。我們經常看到一些新聞網,上面會掛許多iframe廣告, 這些頁面一開始加載時就很卡,也是這緣故.
此外還有object元素, 用來加載flash
等等,這些東西都會影響到DOM樹的構建過程.因此在這時候,當我們貿貿然,使用getElementById, getElementsByTagName獲取元素,然后操作它們, 就會有很大機率碰到 元素為null的 異常. 這時, 目標元素還可以沒有轉換為DOM節點, 還只是一個普通的字符串呢!
我們又不能隨意寫一個
setTimeout(function(){
document.getElementById("xxx").style.left = "80px"
}, 3000)
這完全是靠蒙, 可能有效, 也可能失敗. 因此獲得 所有標簽都轉換為DOM節點的時機就非常重要.
很早期, 瀏覽器提供了一個window.onload方法,但這東西是等到所有標簽變成DOM,並且外部資源,圖片,背景音樂什么都加載好才觸發, 時間上有點晚.
幸好,瀏覽器提供了一個document.readyState屬性,當它變成complete時,說明這時機到了
但這是一個屬性,不是一個事件,需要使用不太精確的setInterval輪詢
在標簽瀏覽器, W3C終於紳士地提供了一個DOMContentLoaded事件;在舊式IE下,也可以勉強使用onreadystatechange事件模擬, 直接某一天,有個外國大牛發掘出doScroll這個偉大的hack, 它讓我們在IE下更接近DOMContentLoaded的效果
function IEContentLoaded (w, fn) {
var d = w.document, done = false,
// 只執行一次用戶的回調函數init()
init = function () {
if (!done) {
done = true;
fn();
}
};
(function () {
try {
// DOM樹未創建完之前調用doScroll會拋出錯誤
d.documentElement.doScroll('left');
} catch (e) {
//延遲再試一次~
setTimeout(arguments.callee, 50);
return;
}
// 沒有錯誤就表示DOM樹創建完畢,然后立馬執行用戶回調
init();
})();
//監聽document的加載狀態
d.onreadystatechange = function() {
// 如果用戶是在domReady之后綁定的函數,就立馬執行
if (d.readyState == 'complete') {
d.onreadystatechange = null;
init();
}
};
}
這里有一些主流框架對domReady的實現
其實都大同小異, 關鍵是 設置一個數組, 當domReady這個時刻沒有到時,先將回調放到數組里; 然后是各種檢測domReady的方法(如DOMContentLoaded, onreadystatechange, doScroll hack, document.readyState輪詢 ), 一旦到了,就執行這個數組所有回調, 並且以后用戶再進入這個方法, 就不放數組,直接執行.
上完這課,以后大家操作節點的邏輯,一定要寫在domReady回調中啊
