從setTimeout/setInterval看JS執行緒

發表於2018-04-19

最近專案中遇到了一個場景,其實很常見,就是定時獲取介面重新整理資料。那麼問題來了,假設我設定的定時時間為1s,而資料介面返回大於1s,應該用同步阻塞還是非同步?我們先整理下js中定時器的相關知識,再來看這個問題。

初識setTimeout 與 setInterval

先來簡單認識,後面我們試試用setTimeout 實現 setInterval 的功能

setTimeout 延遲一段時間執行一次 (Only one)

setInterval 每隔一段時間執行一次 (Many times)

setTimeout和setInterval的延時最小間隔是4ms(W3C在HTML標準中規定);在JavaScript中沒有任何程式碼是立刻執行的,但一旦程式空閒就儘快執行。這意味著無論是setTimeout還是setInterval,所設定的時間都只是n毫秒被新增到佇列中,而不是過n毫秒後立即執行。

程式與執行緒,傻傻分不清楚

為了講清楚這兩個抽象的概念,我們借用阮大大借用的比喻,先來模擬一個場景:

這裡有一個大型工廠
工廠裡有若干車間,每次只能有一個車間在作業
每個車間裡有若干房間,有若干工人在流水線作業

那麼:

一個工廠對應的就是計算機的一個CPU,平時講的多核就代表多個工廠
每個工廠裡的車間,就是程式,意味著同一時刻一個CPU只執行一個程式,其餘程式在怠工
這個執行的車間(程式)裡的工人,就是執行緒,可以有多個工人(執行緒)協同完成一個任務
車間(程式)裡的房間,代表記憶體。

再深入點:

車間(程式)裡工人可以隨意在多個房間(記憶體)之間走動,意味著一個程式裡,多個執行緒可以共享記憶體
部分房間(記憶體)有限,只允許一個工人(執行緒)使用,此時其他工人(執行緒)要等待
房間裡有工人進去後上鎖,其他工人需要等房間(記憶體)裡的工人(執行緒)開鎖出來後,才能才進去,這就是互斥鎖(Mutual exclusion,縮寫 Mutex)
有些房間只能容納部分的人,意味著部分記憶體只能給有限的執行緒

再再深入:

如果同時有多個車間作業,就是多程式
如果一個車間裡有多個工人協同作業,就是多執行緒
當然不同車間之間的工人也可以有相互協作,就需要協調機制

JavaScript 單執行緒

總所周知,JavaScript 這門語言的核心特徵,就是單執行緒(是指在JS引擎中負責解釋和執行JavaScript程式碼的執行緒只有一個)。這和 JavaScript 最初設計是作為一門 GUI 程式語言有關,最初用於瀏覽器端,單一執行緒控制 GUI 是很普遍的做法。但這裡特別要劃個重點,雖然JavaScript是單執行緒,但瀏覽器是多執行緒的!!!例如Webkit或是Gecko引擎,可能有javascript引擎執行緒、介面渲染執行緒、瀏覽器事件觸發執行緒、Http請求執行緒,讀寫檔案的執行緒(例如在Node.js中)。ps:可能要總結一篇瀏覽器渲染的文章了。

HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。

同步與非同步,傻傻分不清楚

之前阮大大寫了一篇《JavaScript 執行機制詳解:再談Event Loop》,然後被樸靈評註了,特別是同步非同步的理解上,兩位大牛有很大的歧義。

同步(synchronous):假如一個函式返回時,呼叫者就能夠得到預期結果(即拿到了預期的返回值或者看到了預期的效果),這就是同步函式。

非同步(asynchronous):假如一個函式返回時,呼叫者不能得到預期結果,需要通過一定手段才能獲得,這就是非同步函式。

非同步構成要素

一個非同步過程通常是這樣的:主執行緒發起一個非同步請求,相應的工作執行緒(比如瀏覽器的其他執行緒)接收請求並告知主執行緒已收到(非同步函式返回);主執行緒可以繼續執行後面的程式碼,同時工作執行緒執行非同步任務;工作執行緒完成工作後,通知主執行緒;主執行緒收到通知後,執行一定的動作(呼叫回撥函式)。

發起(註冊)函式 – 發起非同步過程
回撥函式 – 處理結果

通訊機制

非同步過程的通訊機制:工作執行緒將訊息放到訊息佇列,主執行緒通過事件迴圈過程去取訊息。

訊息佇列 Message Queue

一個先進先出的佇列,存放各類訊息。

事件迴圈 Event Loop

主執行緒(js執行緒)只會做一件事,就是從訊息佇列裡面取訊息、執行訊息,再取訊息、再執行。訊息佇列為空時,就會等待直到訊息佇列變成非空。只有當前的訊息執行結束,才會去取下一個訊息。這種機制就叫做事件迴圈機制Event Loop,取一個訊息並執行的過程叫做一次迴圈。bV5mEF

工作執行緒是生產者,主執行緒是消費者。工作執行緒執行非同步任務,執行完成後把對應的回撥函式封裝成一條訊息放到訊息佇列中;主執行緒不斷地從訊息佇列中取訊息並執行,當訊息佇列空時主執行緒阻塞,直到訊息佇列再次非空。

setTimeout(function, 0) 發生了什麼

其實到這兒,應該能很好解釋setTimeout(function, 0) 這個常用的“奇技淫巧”了。很簡單,就是為了將function裡的任務非同步執行,0不代表立即執行,而是將任務推到訊息佇列的最後,再由主執行緒的事件迴圈去呼叫它執行。

HTML5 中規定setTimeout 的最小時間不是0ms,而是4ms。

setInterval 缺點

再次強調,定時器指定的時間間隔,表示的是何時將定時器的程式碼新增到訊息佇列,而不是何時執行程式碼。所以真正何時執行程式碼的時間是不能保證的,取決於何時被主執行緒的事件迴圈取到,並執行。

那麼顯而易見,上面這段程式碼意味著,每隔N秒把function事件推到訊息佇列中,什麼時候執行?母雞啊!bV5yai

上圖可見,setInterval每隔100ms往佇列中新增一個事件;100ms後,新增T1定時器程式碼至佇列中,主執行緒中還有任務在執行,所以等待,some event執行結束後執行T1定時器程式碼;又過了100ms,T2定時器被新增到佇列中,主執行緒還在執行T1程式碼,所以等待;又過了100ms,理論上又要往佇列裡推一個定時器程式碼,但由於此時T2還在佇列中,所以T3不會被新增,結果就是此時被跳過;這裡我們可以看到,T1定時器執行結束後馬上執行了T2程式碼,所以並沒有達到定時器的效果。

綜上所述,setInterval有兩個缺點:

使用setInterval時,某些間隔會被跳過;
可能多個定時器會連續執行;

鏈式setTimeout

警告:在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當一個函式必須呼叫自身的時候, 避免使用 arguments.callee(), 通過要麼給函式表示式一個名字,要麼使用一個函式宣告.

上述函式每次執行的時候都會建立一個新的定時器,第二個setTimeout使用了arguments.callee()獲取當前函式的引用,並且為其設定另一個定時器。好處:

在前一個定時器執行完前,不會向佇列插入新的定時器(解決缺點一)
保證定時器間隔(解決缺點二)

So…

回顧最開始的業務場景的問題,用同步阻塞還是非同步,答案已經出來了…

PS:其實還有macrotask與microtask等知識點沒有提到,總結了那麼多,其實JavaScript深入下去還有很多,任重而道遠呀。

 

相關文章