說點javascript的加載、解析、執行對瀏覽器渲染的影響
javascript的加載方式,總得來說是在頁面上使用script來聲明,以及動態的加載這些方式,而動態的加載,在很多js庫中都能夠很好的去處理,從而不至於阻塞其他資源的加載,並與其並行加載下來。這樣的動態異步的加載方式羅列起來有:Ajax的方式、DOM Element Insert、Iframe、document.write、defer等等。這些都能夠很好的處理js在加載的時候不會阻塞資源加載的問題,但是,js的執行仍然會阻塞瀏覽器的渲染。或許這是不得不需要付出的代價,有沒有一些方式去緩解js在執行的時候對瀏覽器的渲染所造成的延遲的影響呢?
首先,讓我們從UI Thread、UI Queue、UI Upate的角度去分析整個頁面和javascript的行為。瀏覽器在加載HTML到最后呈現出來的這段過程,整個就是一個UI Thread進程,這個UI Thread里是一個瀏覽器的響應隊列,而UI Queue則是瀏覽器的行為隊列,包括UI Update,javascript的執行行為。那么在頁面加載的過程中,UI Queue里面就儲存了UI Update、js加載、js解析、js執行等。不管是UI Thread,還是UI Queue,都是按照順序來的。頁面在剛開始未遇到js的加載和執行的時候,是UI Update的一個過程,一旦遇到js,就會等待js的加載、解析、執行完畢之后,接着又開始UI Update,如此這樣的一個相應順序。
那么當js的加載是異步了之后呢,這個時候,js的加載和解析不會在UI Queue的執行隊列里,而是等到js加載、解析完畢,一到執行的時候,就添加到UI Queue隊列中,從而阻塞了UI Queue中后面的行為(UI Update)的執行。這就是開頭所說的在js的執行期總會阻塞頁面的渲染,也就是UI Update。但是這個動態加載的方式跟普通在頁面中使用script標簽來加載的時候的優勢是:將js的加載、解析的行為獨立出來,不影響瀏覽器的UI Update,知道執行期才阻塞。這就很多的程度上加快了頁面的渲染和展現。
再來說說defer和async屬性,defer屬性最開始是為IE所實現的,現代的瀏覽器也都逐漸實現了,那么它的工作原理是什么呢。它的作用跟上面動態加載的優勢很相同,js的加載和解析是獨立的,但是有一點不同的是:使用了defer屬性之后,js代碼會等到DOM加載完成的時候才執行,這個時候就又更大限度的讓瀏覽器盡快的UI Update部分完成,讓js的執行對瀏覽器的渲染所造成的影響更小化。那么async呢,是跟上面所說的動態加載js的優勢是一樣的,也是獨立js的加載、解析,解析完成的時候就立即添加到UI Queue隊列中去等待執行,阻塞后面的UI Update行為。
那么怎樣將js的執行對UI Update的影響更小呢?使用HTML5的Worker機制吧,將復雜的js執行交給Worker去執行,Worker是一個獨立的js執行環境,這個環境里沒有window、DOM的概念,只是用來處理復雜的運算,再將處理后的結果通過postMessage方法發送會瀏覽器,Worker和頁面之間通過postMessage方法來實現跟瀏覽器的數據交換。這樣,將復雜運算交給Worker之后,在頁面中所執行的js代碼,將會是大大的減小,減小到只需要處理一個onmessage事件,將頁面的js執行時間最小化。
杯具的還是defer、async、Worker還不能夠完全的被目前市面上流行的瀏覽器所兼容,期待瀏覽器統一的那一天。
當大家使用window.onload執行一個函數時,必須要等到頁面上的圖片等信息全部加載完畢之后才執行的。但很多時候圖片的數量比較多,所以需要很多時間下載。更令人尷尬的是,當網頁文檔(或者說Dom)已經加載完畢,而圖片尚未加載完畢,很多用戶已經開始瀏覽網頁,但這時很多由window.onload所觸發的函數不能執行,這就導致一部分功能不能完美地給用戶使用,更嚴重的是會給用戶留下不好的印象!
現在,我們來研究一下如何解決這個問題,解決方法就是在DOM加載完畢之后就執行程序。
先介紹兩個人。一,jquery的作者:John Resig;二,javascript的世界級大師:dean edwards。(大家要記住這兩位天才!)
jquery里有專門解決DOM加載的函數$(document).ready()(簡寫就是$(fn)),非常好用!John Resig在《Pro JavaScript Techniques》里,有這樣一個方法處理DOM加載,原理就是通過document&& document.getElementsByTagName &&document.getElementById&& document.body 去判斷Dom樹是否加載完畢。代碼如下:
// 如果DOM加載完畢,馬上執行函數if ( domReady.done ) return f();
// 假如我們已增加一個函數
if ( domReady.timer ) {
// 把它加入待執行的函數清單中
domReady.ready.push( f );
} else {
// 為頁面加載完成綁定一個事件,
// 為防止它最先完成. 使用 addEvent(下面列出).
addEvent( window, “load”, isDOMReady );
// 初始化待執行的函數的數組
domReady.ready = [ f ];
// 經可能快地檢查Dom是否已可用
domReady.timer = setInterval( isDOMReady, 13 );
}
}
// 檢查Dom是否已可操作
function isDOMReady() {
// 假如已檢查出Dom已可用, 忽略
if ( domReady.done ) return false;
// 檢查若干函數和元素是否可用
if ( document && document.getElementsByTagName && document.getElementById && document.body ) {
// 假如可用, 停止檢查
clearInterval( domReady.timer );
domReady.timer = null;
// 執行所有等待的函數
for ( var i = 0; i < domReady.ready.length; i++ )
domReady.ready[i]();
// 記錄在此已經完成
domReady.ready = null;
domReady.done = true;
}
}
// 由 Dean Edwards 在2005 所編寫addEvent/removeEvent,
// 由 Tino Zijdel整理
// http://dean.edwards.name/weblog/2005/10/add-event/
//優點是1.可以在所有瀏覽器工作;
//2.this指向當前元素;
//3.綜合了所有瀏覽器防止默認行為和阻止事件冒泡的的函數
//缺點就是僅在冒泡階段工作
function addEvent(element, type, handler) {
// assign each event handler a unique ID
if (!handler.$$guid) handler.$$guid = addEvent.guid++;
// create a hash table of event types for the element
if (!element.events) element.events = {};
// create a hash table of event handlers for each element/event pair
var handlers = element.events[type];
if (!handlers) {
handlers = element.events[type] = {};
// store the existing event handler (if there is one)
if (element["on" + type]) {
handlers[0] = element["on" + type];
}
}
// store the event handler in the hash table
handlers[handler.$$guid] = handler;
// assign a global event handler to do all the work
element["on" + type] = handleEvent;
};
// a counter used to create unique IDs
addEvent.guid = 1;
function removeEvent(element, type, handler) {
// delete the event handler from the hash table
if (element.events && element.events[type]) {
delete element.events[type][handler.$$guid];
}
};
function handleEvent(event) {
var returnValue = true;
// grab the event object (IE uses a global event object)
event = event || fixEvent(window.event);
// get a reference to the hash table of event handlers
var handlers = this.events[event.type];
// execute each event handler
for (var i in handlers) {
this.$$handleEvent = handlers[i];
if (this.$$handleEvent(event) === false) {
returnValue = false;
}
}
return returnValue;
};
function fixEvent(event) {
// add W3C standard event methods
event.preventDefault = fixEvent.preventDefault;
event.stopPropagation = fixEvent.stopPropagation;
return event;
};
fixEvent.preventDefault = function() {
this.returnValue = false;
};
fixEvent.stopPropagation = function() {
this.cancelBubble = true;
};
//
還有一個估計由幾個外國大師合作寫的,實現同樣功能。
/*
* (c)2006 Jesse Skinner/Dean Edwards/Matthias Miller/John Resig
* Special thanks to Dan Webb's domready.js Prototype extension
* and Simon Willison's addLoadEvent
*
* For more info, see:
* http://www.thefutureoftheweb.com/blog/adddomloadevent
* http://dean.edwards.name/weblog/2006/06/again/
* http://www.vivabit.com/bollocks/2006/06/21/a-dom-ready-extension-for-prototype
* http://simon.incutio.com/archive/2004/05/26/addLoadEvent
*
*
* To use: call addDOMLoadEvent one or more times with functions, ie:
*
* function something() {
* // do something
* }
* addDOMLoadEvent(something);
*
* addDOMLoadEvent(function() {
* // do other stuff
* });
*
*/
addDOMLoadEvent = (function(){
// create event function stack
var load_events = [],
load_timer,
script,
done,
exec,
old_onload,
init = function () {
done = true;
// kill the timer
clearInterval(load_timer);
// execute each function in the stack in the order they were added
while (exec = load_events.shift())
exec();
if (script) script.onreadystatechange = '';
};
return function (func) {
// if the init function was already ran, just run this function now and stop
if (done) return func();
if (!load_events[0]) {
// for Mozilla/Opera9
if (document.addEventListener)
document.addEventListener("DOMContentLoaded", init, false);
// for Internet Explorer
/*@cc_on @*/
/*@if (@_win32)
document.write("<script id=__ie_onload defer src=//0><\/scr"+"ipt>");
script = document.getElementById("__ie_onload");
script.onreadystatechange = function() {
if (this.readyState == "complete")
init(); // call the onload handler
};
/*@end @*/
// for Safari
if (/WebKit/i.test(navigator.userAgent)) { // sniff
load_timer = setInterval(function() {
if (/loaded|complete/.test(document.readyState))
init(); // call the onload handler
}, 10);
}
// for other browsers set the window.onload, but also execute the old window.onload
old_onload = window.onload;
window.onload = function() {
init();
if (old_onload) old_onload();
};
}
load_events.push(func);
}
})();
