setTimeout(fn, 0) 的作用

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

setTimeout(fn, 0) 的作用

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


線程

瀏覽器的內核是多線程的,它們在內核控制下相互配合以保持同步,一個瀏覽器通常由以下常駐線程組成: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)。

舉個例子:

setTimeout(fn, 0) 的作用

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。

setTimeout(fn, 0) 的作用

任務隊列:任務(消息)隊列是一個先進先出的隊列,它裡面存放著各種任務(消息)。

事件循環(event loop):事件循環是指主線程重複從任務(消息)隊列中取任務(消息)、執行的過程。取一個任務(消息)並執行的過程叫做一次循環。

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

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

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

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

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

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

setTimeout(fn, 0) 的作用

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

再來看一下這張圖

setTimeout(fn, 0) 的作用

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


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)的特性,修正瀏覽器的任務順序。


分享到:


相關文章: