從setTimeout-setInterval看JS線程

最近項目中遇到了一個場景,其實很常見,就是定時獲取接口刷新數據。那麼問題來了,假設我設置的定時時間爲1s,而數據接口返回大於1s,應該用同步阻塞仍是異步?咱們先整理下js中定時器的相關知識,再來看這個問題。

初識setTimeout 與 setInterval

先來簡單認識,後面咱們試試用setTimeout 實現 setInterval 的功能
  • setTimeout 延遲一段時間執行一次 (Only one)
setTimeout(function, milliseconds, param1, param2, ...)
clearTimeout() // 阻止定時器運行

e.g.
setTimeout(function(){ alert("Hello"); }, 3000); // 3s後彈出
  • setInterval 每隔一段時間執行一次 (Many times)
setInterval(function, milliseconds, param1, param2, ...)

e.g.
setInterval(function(){ alert("Hello"); }, 3000); // 每隔3s彈出
setTimeout和setInterval的延時最小間隔是4ms(W3C在HTML標準中規定);在JavaScript中沒有任何代碼是馬上執行的,但一旦進程空閒就儘快執行。這意味着不管是setTimeout仍是setInterval,所設置的時間都只是n毫秒被添加到隊列中,而不是過n毫秒後當即執行。

進程與線程,傻傻分不清楚

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

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

那麼:html

  • 一個工廠對應的就是計算機的一個CPU,平時講的多核就表明多個工廠
  • 每一個工廠裏的車間,就是進程,意味着同一時刻一個CPU只運行一個進程,其他進程在怠工
  • 這個運行的車間(進程)裏的工人,就是線程,能夠有多個工人(線程)協同完成一個任務
  • 車間(進程)裏的房間,表明內存。

再深刻點:java

  • 車間(進程)裏工人能夠隨意在多個房間(內存)之間走動,意味着一個進程裏,多個線程能夠共享內存
  • 部分房間(內存)有限,只容許一個工人(線程)使用,此時其餘工人(線程)要等待
  • 房間裏有工人進去後上鎖,其餘工人須要等房間(內存)裏的工人(線程)開鎖出來後,才能才進去,這就是互斥鎖(Mutual exclusion,縮寫 Mutex)
  • 有些房間只能容納部分的人,意味着部份內存只能給有限的線程

再再深刻:git

  • 若是同時有多個車間做業,就是多進程
  • 若是一個車間裏有多個工人協同做業,就是多線程
  • 固然不一樣車間之間的工人也能夠有相互協做,就須要協調機制

JavaScript 單線程

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

HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。

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

以前阮大大寫了一篇 《JavaScript 運行機制詳解:再談Event Loop》,而後被 樸靈評註了,特別是同步異步的理解上,兩位大牛有很大的歧義。
  • 同步(synchronous):假如一個函數返回時,調用者就可以獲得預期結果(即拿到了預期的返回值或者看到了預期的效果),這就是同步函數。
e.g.
alert('立刻能看到我拉');
console.log('也能立刻看到我哦');
  • 異步(asynchronous):假如一個函數返回時,調用者不能獲得預期結果,須要經過必定手段才能得到,這就是異步函數。
e.g.
setTimeout(function() {
    // 過一段時間才能執行我哦
}, 1000);

異步構成要素

一個異步過程一般是這樣的:主線程發起一個異步請求,相應的工做線程(好比瀏覽器的其餘線程)接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;工做線程完成工做後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。
  • 發起(註冊)函數 -- 發起異步過程
  • 回調函數 -- 處理結果
e.g.
setTimeout(fn, 1000);
// setTimeout就是異步過程的發起函數,fn是回調函數

通訊機制

異步過程的通訊機制:工做線程將消息放到消息隊列,主線程經過事件循環過程去取消息。

消息隊列 Message Queue

一個先進先出的隊列,存放各種消息。

事件循環 Event Loop

主線程(js線程)只會作一件事,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。消息隊列爲空時,就會等待直到消息隊列變成非空。只有當前的消息執行結束,纔會去取下一個消息。這種機制就叫作事件循環機制 Event Loop,取一個消息並執行的過程叫作一次循環。

圖片描述

工做線程是生產者,主線程是消費者。工做線程執行異步任務,執行完成後把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息並執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。

setTimeout(function, 0) 發生了什麼

其實到這兒,應該能很好解釋setTimeout(function, 0) 這個經常使用的「奇技淫巧」了。很簡單,就是爲了將function裏的任務異步執行,0不表明當即執行,而是將任務推到消息隊列的最後,再由主線程的事件循環去調用它執行。編程

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

setInterval 缺點

再次強調,定時器指定的時間間隔,表示的是什麼時候將定時器的代碼添加到 消息隊列,而 不是什麼時候執行代碼。因此真正什麼時候執行代碼的時間是不能保證的,取決於什麼時候被主線程的事件循環取到,並執行。
setInterval(function, N)

那麼顯而易見,上面這段代碼意味着,每隔N秒把function事件推到消息隊列中,何時執行?母雞啊!瀏覽器

圖片描述

上圖可見,setInterval每隔100ms往隊列中添加一個事件;100ms後,添加T1定時器代碼至隊列中,主線程中還有任務在執行,因此等待,some event執行結束後執行T1定時器代碼;又過了100ms,T2定時器被添加到隊列中,主線程還在執行T1代碼,因此等待;又過了100ms,理論上又要往隊列裏推一個定時器代碼,但因爲此時T2還在隊列中,因此T3不會被添加,結果就是此時被跳過;這裏咱們能夠看到,T1定時器執行結束後立刻執行了T2代碼,因此並無達到定時器的效果。多線程

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

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

鏈式setTimeout

setTimeout(function () {
    // 任務
    setTimeout(arguments.callee, interval);
}, interval)
警告:在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當一個函數必須調用自身的時候, 避免使用 arguments.callee(), 經過要麼給函數表達式一個名字,要麼使用一個函數聲明.

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

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

So...

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

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


參考:

進程與線程的一個簡單解釋 -- 阮大大

【譯】JavaScript 如何工做的: 事件循環和異步編程的崛起 + 5 個關於如何使用 async/await 編寫更好的技巧

已同步至我的博客- 軟硬皆施
Github 歡迎star :)
相關文章
相關標籤/搜索