之前已經有兩篇隨筆提到了event loop,一篇是事件機制,一篇是tasks和microtasks,但是里面的event loop都是文字描述,很難說細,邏輯也只是簡單的提了一遍。其實之前也是通過阮一峰老師的一篇網絡日志:再談event loop,然后寫了點自己的想法。但是總感覺里面一些細節沒有提到,像微任務隊列這種。后來通過查看了一些國外的文檔,尤其是一些谷歌Chrome開發人員的技術文檔,並且結合了whatwg的一些HTML標准,這才有了點較全面的認識,這里把它記錄下來。
這里先來看一段代碼,這是HTML規范里提到的:
eventLoop = { taskQueues: { events: [], // UI events from native GUI framework parser: [], // HTML parser callbacks: [], // setTimeout, requestIdleTask resources: [], // image loading domManipulation: [] }, microtaskQueue: [ ], nextTask: function() { // Spec says: // "Select the oldest task on one of the event loop's task queues" // Which gives browser implementers lots of freedom // Queues can have different priorities, etc. for (let q of taskQueues) if (q.length > 0) return q.shift(); return null; }, executeMicrotasks: function() { if (scriptExecuting) return; let microtasks = this.microtaskQueue; this.microtaskQueue = []; for (let t of microtasks) t.execute(); }, needsRendering: function() { return vSyncTime() && (needsDomRerender() || hasEventLoopEventsToDispatch()); }, render: function() { dispatchPendingUIEvents(); resizeSteps(); scrollSteps(); mediaQuerySteps(); cssAnimationSteps(); fullscreenRenderingSteps(); animationFrameCallbackSteps(); intersectionObserverSteps(); while (resizeObserverSteps()) { updateStyle(); updateLayout(); } paint(); } } while(true) { task = eventLoop.nextTask(); if (task) { task.execute(); } eventLoop.executeMicrotasks(); if (eventLoop.needsRendering()) eventLoop.render(); }
事件循環並沒有多說關於什么時候dispatch event:
1,每一個queue(隊列)中的事件都是按順序執行;
2,事件可以直接dispatch,繞過task queues(任務隊列)
2,微任務是在一個task執行完成后立即執行;
3,渲染部分循環是在vSync上執行,並且按以下順序傳遞事件:
1️⃣分派待處理的UI事件,2️⃣resize事件,3️⃣scroll滾動事件,4️⃣mediaquery監聽者(css @media),5️⃣CSSAnimation事件,6️⃣Observers,7️⃣requestAnimationFrame
下面來詳細說一下
UI events有兩類:
1,Discrete,俗稱離散事件,就是那些不連續的,比如(mousedown,mouseup,touchstart,touchend)等
2,Continuous,俗稱連續事件,就是連續的,比如(mousemove,mousewheel,touchmove,wheel)等
兩種事件的注意點不同:
1,連續事件:一個UI event task queue中,相匹配的連續事件(比如持續更新position屬性,或者持續改變大小),可能會合並,不管那些合並的事件是否被dispatch了,因為有的還沒有被dispatch,或者排在了隊列的后面。
2,離散事件:如果從硬件接收到了離散事件,就必須盡快dispatch,如果此時隊列中有連續事件,就必須立即運行所有的連續事件,以防止離散事件的延遲。也就是說,觸發離散事件的時候,連續事件必定已經全部dispatch完畢。
不同的瀏覽器對事件循環的順序是不同的,我還是習慣以谷歌為准,因為谷歌瀏覽器是最接近規范的
下面列舉一些谷歌dispatch event是通過哪些方法來dispatch的:
1,DOMWindowEventQueue
由計時器觸發
示例事件:window.storage,window.hashchange,document.selectionchange
2,ScriptedAnimationController
由Frame調用的BeginMainFrame函數觸發,同時還管理requestAnimationFrame請求
示例事件:Animation.finish,Animation.cancel,CSSAnimation.animationstart,CSSAnimation.animationiteration(CSSAnimation)
3,Custom dispatch
觸發器不同:OS events操作系統事件,timers定時器,文檔/元素生命周期事件
Custom dispatch event 不通過隊列,他們直接被觸發(這里我猜想可能就是‘任務隊列’的隱藏概念,他們不會排在普通隊列中,而是單獨去了另一個特殊的隊列執行,這里就是‘任務隊列’)
4,Microtask隊列
微任務通常由EndOfTaskRunner.didProcessTask()觸發,任務由TaskQueueManager運行,每當task完成時,微任務隊列就會執行
示例事件:image.onerror,image.onload
微任務也包括Promise callbacks,這里注意,Promise的回調才是真正的微任務,之前說的可能不嚴謹,執行promise的時候本身還是正常task
5,主線程事件隊列
連續事件會被合並處理。
關於Timer有幾個方法:
requestIdleCallback
這個方法只有瀏覽器空閑的時候才會有內部的timer觸發
requestAnimationFrame
由ScriptedAnimationController觸發,這個方法挺重要的,主要是用來解決setTimeout和setInterval無法完成的動畫效果。
Timers:setTimeout,setInterval
由運行在TaskQueue primitive上的WebTaskRunner觸發的。
Observers
觀察者分為兩種,MutationObserver和IntersectionObserver。
MutationObserver:屬於微任務,主要是負責監聽DOM節點內的變化,前一篇隨筆里有提到;
IntersectionObserver:屬於輪詢,可以異步監聽目標元素與其祖先或視窗(viewport)交叉狀態的手段。具體用法不多介紹,不過它可以用來實現懶加載、無限滾動,監聽元素是否在視窗中,或者已經出現了多少內容。
Promises
執行完成后,回調會放到微任務隊列中去。
以上就是event loop的全部相關內容。
========= 這條是分割線 ========
2018年2月14日情人節
其實結合上一篇‘再談tasks和microtasks’
上面提到微任務是在一個task執行完成之后才會執行,而前一篇里提到當JS棧清空時微任務才會執行,但是存在一個task中JS棧會清空的情況(例如那個click冒泡事件),而一個task並未執行完成。這里面就會有一個矛盾點,微任務並沒有在task執行完成后執行,而是中途就執行了。
針對此問題,我私信了Google Chrome的developer,Jake Archibald,感謝他不吝指教:
那其實很明確了,微任務是否執行完全取決於JS stack是否為空,而task是否執行完成,這並不重要。一次任務中可能會有多個事件監聽器,每次執行完一次事件監聽,JS stack就會空一次,這時候就會執行微任務了。
===================================
2018.02.23 下午補充:
nodejs里也有事件循環的改變,它是nodejs自身的執行模型:
它是一個類似於while(true)的循環,每次循環都會去詢問是否有事件需要處理,如果有,取出一個事件,查看該事件是否有對應的回調函數,如果有,執行函數然后進入下一個循環。如果不再有事件處理,則退出進程。
如何得知是否有事件需要處理呢?問誰?
觀察者。每一個事件對應一個觀察者,有文件I/O觀察者,網絡I/O觀察者等等,觀察者將事件進行了分類。
事件循環是一個典型的生產者/消費者模型。異步I/O、網絡請求是事件的生產者,為Node提供不同類型的事件,這些事件被傳遞到對應的觀察者那里,事件循環則從觀察者那里取出事件並處理。
在Windows下,這個循環基於IOCP創建,而在*nix下則基於多線程創建。
end