看了這麼久JS,事件佇列你真的懂嗎?
關於JS事件佇列的一些總結
關於任務佇列
其實之所以我們要去關心JS的任務佇列,主要還是因為JS的單執行緒的特質決定。
為什麼JavaScript是單執行緒?
本段來自阮老師的部落格中對JS單執行緒的介紹。
JavaScript語言的一大特點就是單執行緒,也就是說,同一個時間只能做一件事。那麼,為什麼JavaScript不能有多個執行緒呢?這樣能提高效率啊。
JavaScript的單執行緒,與它的用途有關。作為瀏覽器指令碼語言,JavaScript的主要用途是與使用者互動,以及操作DOM。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個執行緒,一個執行緒在某個DOM節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?
所以,為了避免複雜性,從一誕生,JavaScript就是單執行緒,這已經成了這門語言的核心特徵,將來也不會改變。
為了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript指令碼建立多個執行緒,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質。
任務佇列的本質
- 所有同步任務都在主執行緒上執行,形成一個執行棧(execution context stack)。
- 主執行緒之外,還存在一個”任務佇列”(task queue)。只要非同步任務有了執行結果,就在”任務佇列”之中放置一個事件。
- 一旦”執行棧”中的所有同步任務執行完畢,系統就會讀取”任務佇列”,看看裡面有哪些事件。那些對應的非同步任務,於是結束等待狀態,進入執行棧,開始執行。
- 主執行緒不斷重複上面的第三步。
關於 setTimeOut、setImmediate、process.nextTick()的比較
setTimeout()
- 將事件插入到了事件佇列,必須等到當前程式碼(執行棧)執行完,主執行緒才會去執行它指定的回撥函式。
- 當主執行緒時間執行過長,無法保證回撥會在事件指定的時間執行。
- 瀏覽器端每次
setTimeout
會有4ms的延遲,當連續執行多個setTimeout
,有可能會阻塞程序,造成效能問題。
setImmediate()
- 事件插入到事件佇列尾部,主執行緒和事件佇列的函式執行完成之後立即執行。和setTimeout(fn,0)的效果差不多。
- 服務端node提供的方法。瀏覽器端最新的api也有類似實現:window.setImmediate,但支援的瀏覽器很少。
process.nextTick()
- 插入到事件佇列尾部,但在下次事件佇列之前會執行。也就是說,它指定的任務總是發生在所有非同步任務之前,當前主執行緒的末尾。
- 大致流程:當前”執行棧”的尾部–>下一次Event Loop(主執行緒讀取”任務佇列”)之前–>觸發process指定的回撥函式。
- 伺服器端node提供的辦法。用此方法可以用於處於非同步延遲的問題。
- 可以理解為:此次不行,預約下次優先執行。
關於消除 setTimeout 延遲的實踐:soon.js
why?
如setTimeout
的介紹所言,瀏覽器端每次setTimeout
會有4ms的延遲,當連續執行多個setTimeout
,有可能會阻塞程序,造成效能問題。
soon.js
就是關於這個問題的一個好的實踐。但其實大多數情況我們不必為這4ms的延遲計較,除非你在一次執行中setTimeout
的次數足夠多。程式碼很短,可以用來學習下。
使用方法
可以參考示例
原始碼:
// See http://www.bluejava.com/4NS/Speed-up-your-Websites-with-a-Faster-setTimeout-using-soon
// 使用 soon.js 處理在瀏覽器端 settimeout(大量呼叫),4ms * n 的延遲問題
var soon = (function() {
var fq = []; // 事件佇列;
function callQueue()
{
while(fq.length) // 執行佇列中事件
{
var fe = fq[0];
fe.f.apply(fe.m,fe.a) // 執行佇列中事件
fq.shift();
}
}
// 非同步執行佇列事件,最大效率
var cqYield = (function() {
// 通過 MutationObserver 來監聽 Dom 來執行回撥,此法最快
if(typeof MutationObserver !== "undefined")
{
var dd = document.createElement("div");
var mo = new MutationObserver(callQueue);
mo.observe(dd, { attributes: true });
return function(fn) { dd.setAttribute("a",0); } // trigger callback to
}
// 如果支援 setImmediate ,採取此策略,其實 setImmediate 和 setTimeout(callQueue,0) 差不多
if(typeof setImmediate !== "undefined")
return function() { setImmediate(callQueue) }
// 沒辦法了,就用 setTimeOut 的辦法
return function() { setTimeout(callQueue,0) }
})();
return function(fn) {
// 佇列事件裝載進一個數組
fq.push({f:fn,a:[].slice.apply(arguments).splice(1),m:this});
if(fq.length == 1) // 在新增第一個條目時,啟動回撥函式
cqYield();
};
})();
分析
其實,值得分析就是一個新的東西–MutationObserver
。
MutationObserver
給開發者們提供了一種能在某個範圍內的DOM樹發生變化時作出適當反應的能力.該API設計用來替換掉在DOM3事件規範中引入的Mutation事件.
簡而言之,就是這個東西比setTimeOut
,setImmediate
快,瀏覽器支援就用它就行了。
MutationObserver
給開發者們提供了一種能在某個範圍內的DOM樹發生變化時作出適當反應的能力.該API設計用來替換掉在DOM3事件規範中引入的Mutation事件.