【 js 基礎 】 setTimeout(fn, 0) 的作用

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

$.fn = {n ready: function(callback){n // dont use "interactive" on IE <= 10 (it can fired premature)n //n // document.readyState:當document文檔正在載入時,返回"loading"。當文檔結束渲染但在載入內嵌資源時,返回"interactive",並引發DOMContentLoaded事件。當文檔載入完成時,返回"complete",並引發load事件。n // document.documentElement.doScroll:IE有個特有的方法doScroll可以檢測DOM是否載入完成。 當頁面未載入完成時,該方法會報錯,直到doScroll不再報錯時,就代表DOM載入完成了n //n // 關於 setTimeout(fn ,0) 的作用 可以參考文章:http://www.cnblogs.com/silin6/p/4333999.htmln if (document.readyState === "complete" ||n (document.readyState !== "loading" && !document.documentElement.doScroll))n setTimeout(function(){ callback($) }, 0)n else {n // 監聽移除事件n var handler = function() {n document.removeEventListener("DOMContentLoaded", handler, false)n window.removeEventListener("load", handler, false)n callback($)n }n document.addEventListener("DOMContentLoaded", handler, false)n window.addEventListener("load", handler, false)n }n return thisn },n}n

時間設為 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),可以從根本上理解單線程的執行過程。

推薦一個神器網站:Loupe 可以用來圖形化調用棧的過程,大家可以把例子在網站上運行一下,好用到瘋掉。

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

舉個例子:

來自(並發模型與Event Loop)

function f(b) {n var a = 12;n return a + b + 35;n}nfunction g(x) {n var m = 4;n return f(m * x);n}ng(21);n

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

再舉個例子:

function test() {n setTimeout(function() {n alert(1)n },1000);n alert(2);n}ntest();n

在執行函數 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』); //函數返回時,就看到了預期的效果:在控制台列印了一個字元串n

非同步函數即如果在函數A返回的時候,調用者還不能夠得到預期結果,而是需要在將來通過一定的手段得到,那麼這個函數就是非同步的。

    例子:

setTimeout(fn, 1000);//setTimeout是非同步過程的發起函數,fn是回調函數。n

  • 任務也分為兩種:同步任務和非同步任務。

    同步任務:在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。

    非同步任務:主線程發起一個非同步請求(即執行非同步函數),相應的工作線程(瀏覽器事件觸發線程、非同步http請求線程等)接收請求並告知主線程已收到(非同步函數返回);主線程可以繼續執行後面的代碼,同時工作線程執行非同步任務;工作線程完成工作後,將完成消息放到任務(消息)隊列,主線程通過事件循環過程去取任務(消息),然後執行一定的動作(調用回調函數)。

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

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

    事件循環中有事件兩個字的原因:任務(消息)隊列中的每條消息實際上都對應著一個事件——dom事件。

    例子:

var button = document.getElement(#btn);nbutton.addEventListener(click,function(e) {n console.log();n})n

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

那麼 任務(消息)到底是什麼呢? 任務(消息)就是註冊非同步任務時添加的回調函數。如果 一個非同步函數沒有回調,那麼他就不會放到任務(消息)隊列里。

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

例子: 工作線程為非同步 http 請求線程即 Ajax 線程

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

再來看一下這張圖

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

三、setTimeout(fn, 0) 的作用

調用 setTimeout 函數會在一個時間段過去後在隊列中添加一個消息。這個時間段作為函數的第二個參數被傳入。如果隊列中沒有其它消息,消息會被馬上處理。但是,如果有其它消息,setTimeout 消息必須等待其它消息處理完。因此第二個參數僅僅表示最少的時間,而非確切的時間。

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

例子

setTimeout(function() {n console.log(1);n },0);n console.log(2)n

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

總結:setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閑時間執行,也就是說,儘可能早得執行。它在"任務隊列"的尾部添加一個事件,因此要等到主線程把同步任務和"任務隊列"現有的事件都處理完,才會得到執行。

在某種程度上,我們可以利用setTimeout(fn,0)的特性,修正瀏覽器的任務順序。

學習並感謝

JavaScript:徹底理解同步、非同步和事件循環(Event Loop)

並發模型與Event Loop

JavaScript下的setTimeout(fn,0)意味著什麼? - linkFly - 博客園

JavaScript 運行機制詳解:再談Event Loop

最後再推一遍 神器,一定要用哦 Loupe
推薦閱讀:

新年伊始也來談談Webfont
阿里媽媽前端技術周刊 2018-01-12

TAG:前端开发 | 前端工程师 | 前端框架 |