最近專案中遇到了一個場景,其實很常見,就是定時獲取介面重新整理資料。那麼問題來了,假設我設定的定時時間為1s,而資料介面返回大於1s,應該用同步阻塞還是非同步?我們先整理下js中定時器的相關知識,再來看這個問題。
初識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單執行緒的本質。
同步與非同步,傻傻分不清楚
之前阮大大寫了一篇《JavaScript 執行機制詳解:再談Event Loop》,然後被樸靈評註了,特別是同步非同步的理解上,兩位大牛有很大的歧義。
同步(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()獲取當前函式的引用,並且為其設定另一個定時器。好處:
在前一個定時器執行完前,不會向佇列插入新的定時器(解決缺點一)
保證定時器間隔(解決缺點二)
So…
回顧最開始的業務場景的問題,用同步阻塞還是非同步,答案已經出來了…
PS:其實還有macrotask與microtask等知識點沒有提到,總結了那麼多,其實JavaScript深入下去還有很多,任重而道遠呀。