setTimeout(fn, 0) 的做用

先看:javascript





js矛盾的單線程異步

都說js是單線程的,但是setTImeout和setInterval是什麼鬼?這不是自相矛盾嗎,然而,不存在的

衆所周知,js是單線程的,但在js中的setTimeout和setInterval函數這哥倆卻能很好的異步 以下
java

123456setTimeout(function(){console.log('若是我在2s後運行')},2*1000);console.log('若是同步,我將被阻塞2s')
瀏覽器

上面的代碼已經很清楚了 可是結果倒是
bash

很顯然,若是咱們js代碼是完徹底全的同步的話,那麼第二個打印應該會被第一個打印阻塞2s,可是並無阻塞主線程,倒是丟下他繼續執行了下去.數據結構

反正別管這麼多,咱們的js就是單線程的,那麼那些異步操做是怎麼完成的呢?答案是咱們的偉大的多線程的Browser,沒錯,雖然js是單線程的,可是瀏覽器是多線程的啊!多線程

當js讀到setTimeout這個函數時,js告訴瀏覽器,嘿 兄弟 我這有個耗時操做 我搞不來 你幫我一下 固然瀏覽器很欣喜的答案了 並開了一個計時器線程,而後js就放心的丟下這個耗時操做 去執行下面的代碼了。
那麼問題就來了
併發

123456setTimeout(function(){console.log('若是我在2s後運行')},0);console.log('若是同步,我將被阻塞2s')
dom

上面的這幾行代碼我只是把時間換成了0而已 來 猜猜他的打印結果異步

好了 不賣關子了函數

wtf? 憑什麼 我等待了0s 就差很少沒等待吧 憑什麼我仍是打印在你後面

這就是扯到咱們js的事件循環隊列了

在js中 咱們全部同步(如今)的任務都在主線程中運行 造成執行棧
在主線程以外 咱們還有一個任務隊列 專門運行異步(將來)執行的代碼
咱們的js會優先讀取執行棧中的同步任務 而後在去讀取任務隊列裏的異步任務 這個過程叫作事件循環(Event Loop) 當咱們的異步任務執行完畢以後 就會在任務隊列中插入一個事件 等待js執行



在 zepto 源碼中,$.fn 對象 有個 ready 函數,其中有這樣一句 setTimeout(fn,0);

$.fn = {
    ready: function(callback){
      // dont use "interactive" on IE <= 10 (it can fired premature)
      //
      // document.readyState:當document文檔正在加載時,返回"loading"。當文檔結束渲染但在加載內嵌資源時,返回"interactive",並引起DOMContentLoaded事件。當文檔加載完成時,返回"complete",並引起load事件。
      // document.documentElement.doScroll:IE有個特有的方法doScroll能夠檢測DOM是否加載完成。 當頁面未加載完成時,該方法會報錯,直到doScroll再也不報錯時,就表明DOM加載完成了
      if (document.readyState === "complete" ||
          (document.readyState !== "loading" && !document.documentElement.doScroll))
        setTimeout(function(){ callback($) }, 0)   //  重點
      else {
        // 監聽移除事件
        var handler = function() {
          document.removeEventListener("DOMContentLoaded", handler, false)
          window.removeEventListener("load", handler, false)
          callback($)
        }
        document.addEventListener("DOMContentLoaded", handler, false)
        window.addEventListener("load", handler, false)
      }
      return this;
    },
}複製代碼

時間設爲 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 執行隊列的尾端(因此用這兩個函數的時候,實際的執行時間是大於或等於指定時間的,不保證能準肯定時的)。

二、javascript 是單線程的,同一個時間只能作一件事。

這裏說一下 js調用棧(call stack),能夠從根本上理解單線程的執行過程。
推薦一個神器網站:Loupe 能夠用來圖形化調用棧的過程,你們能夠把例子在網站上運行一下,好用到瘋掉。

js 調用棧(call stack):函數被調用時,就會被加入到調用棧頂部,執行結束以後,就會從調用棧頂部移除該函數,這種數據結構的關鍵在於後進先出,即 LIFO(last-in,first-out)。

舉個例子:
來自(併發模型與Event Loop


function f(b) {
    var a = 12;
    return a + b + 35;
}
function g(x) {
    var m = 4;
    return f(m * x);
}
g(21);複製代碼

調用 g 函數 的時候,建立了第一個 堆( Heap ) 棧(stack) 幀 ,包含了 g 的參數和局部變量。當 g 調用 f 的時候,第二個 堆棧幀 就被建立、並置於第一個 堆棧幀 之上,包含了 f 的參數和局部變量。當 f 返回時,最上層的 堆棧幀 就出棧了(剩下 g 函數調用的 堆棧幀 )。當 g 返回的時候,棧就空了。

再舉個例子:

function test() {
    setTimeout(function() {
        alert(1)
    },1000);
    alert(2);
}
test();複製代碼

在執行函數 test 的時候,test 先入棧,若是不給 alert(1)加 setTimeout,那麼 alert(1)第 2 個入棧,最後是 alert(2)。但如今給 alert(1)加上 setTimeout 後,alert(1)就被加入到了一個新的堆棧中等待,並1s後執行,所以實際的執行結果就是先 alert(2),再 alert(1)。

三、任務隊列(消息隊列):

  • 函數分爲兩種:同步和異步。

    同步函數:若是在函數A返回的時候,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),那麼這個函數就是同步的。

例子:

console.log('Hi’); //函數返回時,就看到了預期的效果:在控制檯打印了一個字符串複製代碼

異步函數:即若是在函數A返回的時候,調用者還不可以獲得預期結果,而是須要在未來經過必定的手段獲得,那麼這個函數就是異步的。
    例子:

setTimeout(fn, 1000);//setTimeout是異步過程的發起函數,fn是回調函數。
複製代碼

  • 任務也分爲兩種:同步任務和異步任務。

    同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
    異步任務:主線程發起一個異步請求(即執行異步函數),相應的工做線程(瀏覽器事件觸發線程、異步http請求線程等)接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;工做線程完成工做後,將完成消息放到任務(消息)隊列,主線程經過事件循環過程去取任務(消息),而後執行必定的動做(調用回調函數)。

    圖中主線程即 Stack,任務隊列即 Queue。


  • 任務隊列:任務(消息)隊列是一個先進先出的隊列,它裏面存放着各類任務(消息)。
  • 事件循環(event loop):事件循環是指主線程重複從任務(消息)隊列中取任務(消息)、執行的過程。取一個任務(消息)並執行的過程叫作一次循環。

    事件循環中有事件兩個字的緣由:任務(消息)隊列中的每條消息實際上都對應着一個事件——dom事件。
    例子:

var button = document.getElement('#btn');
button.addEventListener('click',function(e) {
      console.log(1);
});複製代碼

從異步過程的角度看,addEventListener 函數就是異步過程的發起函數,事件監聽器函數就是異步過程的回調函數。事件觸發時,表示異步任務完成,會將事件監聽器函數封裝成一條消息放到消息隊列中,等待主線程執行。


那麼 任務(消息)究竟是什麼呢? 任務(消息)就是註冊異步任務時添加的回調函數。若是 一個異步函數沒有回調,那麼他就不會放到任務(消息)隊列裏。


總結一下過程:主線程在執行完當前循環中的全部代碼後,就會到任務(消息)隊列取出一條消息,並執行它。到此爲止,就完成了工做線程對主線程的通知,回調函數也就獲得了執行。若是一開始主線程就沒有提供回調函數,工做線程就不必通知主線程,從而也不必往消息隊列放消息。

例子: 工做線程爲異步 http 請求線程即 Ajax 線程


最後注意異步過程的回調函數,必定不在當前這一輪事件循環中執行。而是當 這一輪執行完了,主線程空了,再從任務(消息)隊列中取。

再來看一下這張圖


主線程運行的時候,產生堆(heap)和棧(stack),棧中的代碼調用各類外部API,它們在"任務隊列"中加入各類事件(click,load,done)。只要棧中的代碼執行完畢,主線程就會去讀取"任務隊列",依次執行那些事件所對應的回調函數。


3、setTimeout(fn, 0) 的做用

調用 setTimeout 函數會在一個時間段過去後在隊列中添加一個消息。這個時間段做爲函數的第二個參數被傳入。若是隊列中沒有其它消息,消息會被立刻處理。可是,若是有其它消息,setTimeout 消息必須等待其它消息處理完。所以第二個參數僅僅表示最少的時間,而非確切的時間。

零延遲 (Zero delay) 並非意味着回調會當即執行。在零延遲調用 setTimeout 時,其並非過了給定的時間間隔後就立刻執行回調函數。其等待的時間基於隊列里正在等待的消息數量。也就是說,setTimeout()只是將事件插入了任務隊列,必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等好久,因此並無辦法保證回調函數必定會在setTimeout()指定的時間執行。

例子

setTimeout(function() {
     console.log(1);
 },0);
 console.log(2)複製代碼

執行結果2,1。由於只有在執行完第二行之後,主線程空了,纔會去任務隊列中取任務執行回調函數。

總結:setTimeout(fn,0)的含義是,指定某個任務在主線程最先可得的空閒時間執行,也就是說,儘量早得執行。它在"任務隊列"的尾部添加一個事件,所以要等到主線程把同步任務和"任務隊列"現有的事件都處理完,纔會獲得執行。在某種程度上,咱們能夠利用setTimeout(fn,0)的特性,修正瀏覽器的任務順序。

相關文章
相關標籤/搜索