1 $.fn = { 2 ready: function(callback){ 3 // don't use "interactive" on IE <= 10 (it can fired premature) 4 // 5 // document.readyState:當document文檔正在加載時,返回"loading"。當文檔結束渲染但在加載內嵌資源時,返回"interactive",並引發DOMContentLoaded事件。當文檔加載完成時,返回"complete",並引發load事件。 6 // document.documentElement.doScroll:IE有個特有的方法doScroll可以檢測DOM是否加載完成。 當頁面未加載完成時,該方法會報錯,直到doScroll不再報錯時,就代表DOM加載完成了 7 // 8 // 關於 setTimeout(fn ,0) 的作用 可以參考文章:http://www.cnblogs.com/silin6/p/4333999.html 9 if (document.readyState === "complete" || 10 (document.readyState !== "loading" && !document.documentElement.doScroll)) 11 setTimeout(function(){ callback($) }, 0) 12 else { 13 // 監聽移除事件 14 var handler = function() { 15 document.removeEventListener("DOMContentLoaded", handler, false) 16 window.removeEventListener("load", handler, false) 17 callback($) 18 } 19 document.addEventListener("DOMContentLoaded", handler, false) 20 window.addEventListener("load", handler, false) 21 } 22 return this 23 }, 24 }
- GUI 渲染線程:負責渲染瀏覽器界面 HTML 元素,當界面需要重繪(Repaint)或由於某種操作引發回流(reflow)時,該線程就會執行。在 Javascript 引擎運行腳本期間, GUI 渲染線程都是處於掛起狀態的,也就是說被”凍結”。即 GUI 渲染線程與 JS 引擎是互斥的,當JS引擎執行時GUI線程會被掛起,GUI 更新會被保存在一個隊列中等到 JS 引擎空閑時立即被執行。
- javascript 引擎線程:也可以稱為 JS 內核,主要負責處理 Javascript 腳本程序,例如 V8 引擎。Javascript 引擎線程理所當然是負責解析 Javascript 腳本,運行代碼。瀏覽器無論什么時候都只有一個 JS 線程在運行 JS 程序。
- 瀏覽器事件觸發線程:當一個事件被觸發時該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎的處理。這些事件可以是當前執行的代碼塊如定時任務、也可來自瀏覽器內核的其他線程如鼠標點擊、AJAX 異步請求等,但由於JS的單線程關系所有這些事件都得排隊等待 JS 引擎處理。
- 定時觸發器線程:瀏覽器定時計數器並不是由 JavaScript 引擎計數的, 因為 javaScript 引擎是單線程的, 如果處於阻塞線程狀態就會影響記計時的准確, 因此通過單獨線程來計時並觸發定時是更為合理的方案。
- 異步 http 請求線程:在 XMLHttpRequest 在連接后是通過瀏覽器新開一個線程請求, 將檢測到狀態變更時,如果設置有回調函數,異步線程就產生狀態變更事件放到 JavaScript 引擎的處理隊列中等待處理。
例子1:異步請求是由線程 JavaScript 執行線程、HTTP 請求線程 和 事件觸發線程 共同完成的。JavaScript 執行線程 執行異步請求代碼,這時瀏覽器會開一條新的 HTTP 請求線程 來執行請求,JavaScript 執行線程則繼續執行 執行隊列 中剩下的其他任務。然后在未來的某一時刻 事件觸發線程 監視到之前的發起的 HTTP 請求已完成,它就會把完成事件的回調代碼插入到 JavaScript 執行隊列尾部 等待 JavaScript 執行線程空閑時來處理。
例子2:定時觸發(setTimeout 和 setInterval)是由瀏覽器的 定時器線程 執行的定時計數,然后在定時時間結束時把定時處理函數的執行代碼插入到 JavaScript 執行隊列的尾端(所以用這兩個函數的時候,實際的執行時間是大於或等於指定時間的,不保證能准確定時的)。
2、javascript 是單線程的,同一個時間只能做一件事。
這里說一下 js調用棧(call stack),可以從根本上理解單線程的執行過程。
推薦一個神器網站:http://latentflip.com/loupe/ 可以用來圖形化調用棧的過程,大家可以把例子在網站上運行一下,好用到瘋掉。
js 調用棧(call stack):函數被調用時,就會被加入到調用棧頂部,執行結束之后,就會從調用棧頂部移除該函數,這種數據結構的關鍵在於后進先出,即 LIFO(last-in,first-out)。
1 function f(b) { 2 var a = 12; 3 return a + b + 35; 4 } 5 function g(x) { 6 var m = 4; 7 return f(m * x); 8 } 9 g(21);
調用 g 函數 的時候,創建了第一個 堆( Heap ) 棧(stack) 幀 ,包含了 g 的參數和局部變量。當 g 調用 f 的時候,第二個 堆棧幀 就被創建、並置於第一個 堆棧幀 之上,包含了 f 的參數和局部變量。當 f 返回時,最上層的 堆棧幀 就出棧了(剩下 g 函數調用的 堆棧幀 )。當 g 返回的時候,棧就空了。
再舉個例子:
1 function test() { 2 setTimeout(function() { 3 alert(1) 4 },1000); 5 alert(2); 6 } 7 test();
在執行函數 test 的時候,test 先入棧,如果不給 alert(1)加 setTimeout,那么 alert(1)第 2 個入棧,最后是 alert(2)。但現在給 alert(1)加上 setTimeout 后,alert(1)就被加入到了一個新的堆棧中等待,並1s后執行,因此實際的執行結果就是先 alert(2),再 alert(1)。
3、任務隊列(消息隊列):
- 函數分為兩種:同步和異步。
同步函數:如果在函數A返回的時候,調用者就能夠得到預期結果(即拿到了預期的返回值或者看到了預期的效果),那么這個函數就是同步的。
例子:
console.log('Hi’); //函數返回時,就看到了預期的效果:在控制台打印了一個字符串
異步函數即如果在函數A返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那么這個函數就是異步的。
例子:
setTimeout(fn, 1000);//setTimeout是異步過程的發起函數,fn是回調函數。
- 任務也分為兩種:同步任務和異步任務。
同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行后一個任務。
異步任務:主線程發起一個異步請求(即執行異步函數),相應的工作線程(瀏覽器事件觸發線程、異步http請求線程等)接收請求並告知主線程已收到(異步函數返回);主線程可以繼續執行后面的代碼,同時工作線程執行異步任務;工作線程完成工作后,將完成消息放到任務(消息)隊列,主線程通過事件循環過程去取任務(消息),然后執行一定的動作(調用回調函數)。
圖中主線程即 Stack,任務隊列即 Queue。
- 任務隊列:任務(消息)隊列是一個先進先出的隊列,它里面存放着各種任務(消息)。
- 事件循環(event loop):事件循環是指主線程重復從任務(消息)隊列中取任務(消息)、執行的過程。取一個任務(消息)並執行的過程叫做一次循環。
事件循環中有事件兩個字的原因:任務(消息)隊列中的每條消息實際上都對應着一個事件——dom事件。
例子:
1 var button = document.getElement('#btn'); 2 button.addEventListener('click', 3 function(e) { 4 console.log(); 5 });
從異步過程的角度看,addEventListener 函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放到消息隊列中,等待主線程執行。
那么 任務(消息)到底是什么呢? 任務(消息)就是注冊異步任務時添加的回調函數。如果 一個異步函數沒有回調,那么他就不會放到任務(消息)隊列里。
總結一下過程:主線程在執行完當前循環中的所有代碼后,就會到任務(消息)隊列取出一條消息,並執行它。到此為止,就完成了工作線程對主線程的通知,回調函數也就得到了執行。如果一開始主線程就沒有提供回調函數,工作線程就沒必要通知主線程,從而也沒必要往消息隊列放消息。
例子: 工作線程為異步 http 請求線程即 Ajax 線程
最后注意異步過程的回調函數,一定不在當前這一輪事件循環中執行。而是當 這一輪執行完了,主線程空了,再從任務(消息)隊列中取。
再來看一下這張圖
主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各種外部API,它們在"任務隊列"中加入各種事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。
三、setTimeout(fn, 0) 的作用
調用 setTimeout 函數會在一個時間段過去后在隊列中添加一個消息。這個時間段作為函數的第二個參數被傳入。如果隊列中沒有其它消息,消息會被馬上處理。但是,如果有其它消息,setTimeout 消息必須等待其它消息處理完。因此第二個參數僅僅表示最少的時間,而非確切的時間。
零延遲 (Zero delay) 並不是意味着回調會立即執行。在零延遲調用 setTimeout 時,其並不是過了給定的時間間隔后就馬上執行回調函數。其等待的時間基於隊列里正在等待的消息數量。也就是說,setTimeout()只是將事件插入了任務隊列,必須等到當前代碼(執行棧)執行完,主線程才會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等很久,所以並沒有辦法保證回調函數一定會在setTimeout()指定的時間執行。
例子
1 setTimeout(function() { 2 console.log(1); 3 },0); 4 console.log(2);
執行結果2,1。因為只有在執行完第二行以后,主線程空了,才會去任務隊列中取任務執行回調函數。
總結:setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閑時間執行,也就是說,盡可能早得執行。它在"任務隊列"的尾部添加一個事件,因此要等到主線程把同步任務和"任務隊列"現有的事件都處理完,才會得到執行。
在某種程度上,我們可以利用setTimeout(fn,0)的特性,修正瀏覽器的任務順序。