最近項目中遇到了一個場景,其實很常見,就是定時獲取接口刷新數據。那麼問題來了,假設我設置的定時時間爲1s,而數據接口返回大於1s,應該用同步阻塞仍是異步?咱們先整理下js中定時器的相關知識,再來看這個問題。
先來簡單認識,後面咱們試試用setTimeout 實現 setInterval 的功能
setTimeout(function, milliseconds, param1, param2, ...) clearTimeout() // 阻止定時器運行 e.g. setTimeout(function(){ alert("Hello"); }, 3000); // 3s後彈出
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
再深刻點:java
再再深刻:git
總所周知,JavaScript 這門語言的核心特徵,就是單線程(是指在JS引擎中負責解釋和執行JavaScript代碼的線程只有一個)。這和 JavaScript 最初設計是做爲一門 GUI 編程語言有關,最初用於瀏覽器端,單一線程控制 GUI 是很廣泛的作法。但這裏特別要劃個重點,雖然JavaScript是單線程,但瀏覽器是多線程的!!!例如Webkit或是Gecko引擎,可能有javascript引擎線程、界面渲染線程、瀏覽器事件觸發線程、Http請求線程,讀寫文件的線程(例如在Node.js中)。ps:可能要總結一篇瀏覽器渲染的文章了。github
HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。
以前阮大大寫了一篇 《JavaScript 運行機制詳解:再談Event Loop》,而後被 樸靈評註了,特別是同步異步的理解上,兩位大牛有很大的歧義。
e.g. alert('立刻能看到我拉'); console.log('也能立刻看到我哦');
e.g. setTimeout(function() { // 過一段時間才能執行我哦 }, 1000);
一個異步過程一般是這樣的:主線程發起一個異步請求,相應的工做線程(好比瀏覽器的其餘線程)接收請求並告知主線程已收到(異步函數返回);主線程能夠繼續執行後面的代碼,同時工做線程執行異步任務;工做線程完成工做後,通知主線程;主線程收到通知後,執行必定的動做(調用回調函數)。
e.g. setTimeout(fn, 1000); // setTimeout就是異步過程的發起函數,fn是回調函數
異步過程的通訊機制:工做線程將消息放到消息隊列,主線程經過事件循環過程去取消息。
一個先進先出的隊列,存放各種消息。
主線程(js線程)只會作一件事,就是從消息隊列裏面取消息、執行消息,再取消息、再執行。消息隊列爲空時,就會等待直到消息隊列變成非空。只有當前的消息執行結束,纔會去取下一個消息。這種機制就叫作事件循環機制 Event Loop,取一個消息並執行的過程叫作一次循環。
工做線程是生產者,主線程是消費者。工做線程執行異步任務,執行完成後把對應的回調函數封裝成一條消息放到消息隊列中;主線程不斷地從消息隊列中取消息並執行,當消息隊列空時主線程阻塞,直到消息隊列再次非空。
其實到這兒,應該能很好解釋setTimeout(function, 0) 這個經常使用的「奇技淫巧」了。很簡單,就是爲了將function
裏的任務異步執行,0不表明當即執行,而是將任務推到消息隊列的最後,再由主線程的事件循環去調用它執行。編程
HTML5 中規定setTimeout 的最小時間不是0ms,而是4ms。
再次強調,定時器指定的時間間隔,表示的是什麼時候將定時器的代碼添加到 消息隊列,而 不是什麼時候執行代碼。因此真正什麼時候執行代碼的時間是不能保證的,取決於什麼時候被主線程的事件循環取到,並執行。
setInterval(function, N)
那麼顯而易見,上面這段代碼意味着,每隔N秒把function事件推到消息隊列中,何時執行?母雞啊!瀏覽器
上圖可見,setInterval每隔100ms往隊列中添加一個事件;100ms後,添加T1定時器代碼至隊列中,主線程中還有任務在執行,因此等待,some event
執行結束後執行T1定時器代碼;又過了100ms,T2定時器被添加到隊列中,主線程還在執行T1代碼,因此等待;又過了100ms,理論上又要往隊列裏推一個定時器代碼,但因爲此時T2還在隊列中,因此T3不會被添加,結果就是此時被跳過;這裏咱們能夠看到,T1定時器執行結束後立刻執行了T2代碼,因此並無達到定時器的效果。多線程
綜上所述,setInterval有兩個缺點:app
setTimeout(function () { // 任務 setTimeout(arguments.callee, interval); }, interval)
警告:在嚴格模式下,第5版 ECMAScript (ES5) 禁止使用 arguments.callee()。當一個函數必須調用自身的時候, 避免使用 arguments.callee(), 經過要麼給函數表達式一個名字,要麼使用一個函數聲明.
上述函數每次執行的時候都會建立一個新的定時器,第二個setTimeout使用了arguments.callee()獲取當前函數的引用,而且爲其設置另外一個定時器。好處:異步
回顧最開始的業務場景的問題,用同步阻塞仍是異步,答案已經出來了...
PS:其實還有macrotask與microtask等知識點沒有提到,總結了那麼多,其實JavaScript深刻下去還有不少,任重而道遠呀。
參考:
【譯】JavaScript 如何工做的: 事件循環和異步編程的崛起 + 5 個關於如何使用 async/await 編寫更好的技巧
已同步至我的博客- 軟硬皆施
Github 歡迎star :)