【 js 基礎 】【 源碼學習 】 setTimeout(fn, 0) 的作用


在 zepto 源碼中,$.fn 對象 有個 ready 函數,其中有這樣一句  setTimeout(fn,0);
 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 }

時間設為 0 ,就是要立即執行,那為什么還要特意將 fn 套到 setTimeout 里面呢?

 

一、線程
1、瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成:GUI 渲染線程,javascript 引擎線程,瀏覽器事件觸發線程,定時觸發器線程,異步 http 請求線程。
 
  • 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)的特性,修正瀏覽器的任務順序。

 

 

------------- 學會的知識也要時常review ------------
 
 
 
 
最后再推一遍 神器,一定要用哦   http://latentflip.com/loupe/ 

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM