1. 程式人生 > >最近項目中遇到了一個場景,其實很常見,就是定時獲取接口刷新數據。那麽問題來了,假設我設置的定時時間為1s,而數據接口返回大於1s,應該用同步阻塞還是異步?

最近項目中遇到了一個場景,其實很常見,就是定時獲取接口刷新數據。那麽問題來了,假設我設置的定時時間為1s,而數據接口返回大於1s,應該用同步阻塞還是異步?

set timeout pre git plain 異步執行 項目 strip nod

初識setTimeout 與 setInterval

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

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

1 2 3 4 5 setTimeout(function, milliseconds, param1, param2, ...) clearTimeout() // 阻止定時器運行 e.g. setTimeout(function(){ alert("Hello"); }, 3000); // 3s後彈出

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

1 2 3 4 setInterval(function, milliseconds, param1, param2, ...) e.g. setInterval(function(){ alert("Hello"); }, 3000); // 每隔3s彈出

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單線程的本質。

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

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

1 2 3 e.g. alert(‘馬上能看到我拉‘); console.log(‘也能馬上看到我哦‘);

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

1 2 3 4 e.g. setTimeout(function() { // 過一段時間才能執行我哦 }, 1000);

異步構成要素

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

發起(註冊)函數 – 發起異步過程
回調函數 – 處理結果

1 2 3 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 缺點

再次強調,定時器指定的時間間隔,表示的是何時將定時器的代碼添加到消息隊列,而不是何時執行代碼。所以真正何時執行代碼的時間是不能保證的,取決於何時被主線程的事件循環取到,並執行。

1 setInterval(function, N)

那麽顯而易見,上面這段代碼意味著,每隔N秒把function事件推到消息隊列中,什麽時候執行?母雞啊!技術分享圖片

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

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

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

鏈式setTimeout

1 2 3 4 setTimeout(function () { // 任務 setTimeout(arguments.callee, interval); }, interval)

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

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

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

最近項目中遇到了一個場景,其實很常見,就是定時獲取接口刷新數據。那麽問題來了,假設我設置的定時時間為1s,而數據接口返回大於1s,應該用同步阻塞還是異步?