JavaScript:event loop詳解


之前已經有兩篇隨筆提到了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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM