消息隊列和事件循環、宏任務和微任務

1、消息隊列和事件循環

  1. 什麼是事件循環和消息隊列?c++

    頁面中的大部分任務——包括渲染事件、用戶交互事件、JavaScript 腳本執行事件、網絡請求完成和文件讀寫完成事件等——都是在渲染進程的主線程上執行的,爲了協調這些任務有條不紊地在主線程上執行,渲染進程引入了消息隊列和事件循環機制。渲染進程內部會維護多個消息隊列,好比延遲執行隊列和普通地消息隊列,而後主線程採用一個 for 循環,不斷地從這些任務隊列中取出任務並執行任務。json

  2. C++代碼模擬事件循環和消息隊列跨域

    // 隊列
    class TaskQueue {
        public:
        Task takeTask();  // 取出隊列頭部的一個任務
        void pushTask(Task task);  // 添加一個任務到隊列尾部
    }
    
    TaskQueue task_queue;
    void ProcessTask();   // 執行任務
    bool keep_running = true;
    void MainThread() {
        for(;;) {
            Task task = task_queue.takeTask();  // 從消息隊列中讀取一個任務
            ProcessTask(task);
            if(!keep_running)  // 若是設置了退出標誌,呢麼直接退出線程循環
                break;
        }
    }
    
    Task clickTask;   
    task_queue.pushTask(clickTask);  // 添加一個任務到消息隊列中
    複製代碼
  3. 消息隊列中的任務類型瀏覽器

    • 內部消息類型:輸入事件(鼠標滾動、點擊、移動)、微任務、文件讀寫、WebSocket、定時器等。
    • 與頁面相關的事件:JavaScript執行、解析DOM、樣式計算、佈局、CSS動畫等。
  4. 頁面使用單線程的缺點安全

    消息隊列有「先進先出」的特色,放入消息隊列中的任務,須要等前面的任務被執行完,纔會被執行。因此要解決如下兩個問題:網絡

    (1) 如何處理高優先級的任務異步

    • 使用微任務。消息隊列中的任務稱爲宏任務,每一個宏任務中都包含了一個微任務隊列。等宏任務中的主要功能都完成後,渲染引擎不急着去執行下一個宏任務,而是執行當前宏任務中的微任務。

    (2) 如何解決單個任務執行時長太久的問題函數

    • 經過回調,讓 JavaScript 任務滯後執行。
    • 使用 Web Worker,與 DOM 操做無關的任務能夠放在 Web Worker 中執行。

2、setTimeout

  1. setTimeout方法是什麼?工具

    setTimeout方法是一個定時器,用來指定某個函數在多少毫秒後執行。返回一個整數,表示定時器的編號,能夠經過該編號來取消這個定時器。佈局

  2. 瀏覽器怎麼實現 setTimeout ?

    定時器設置的回調函數須要在指定的時間間隔內被調用,但消息隊列中的任務是按照順序執行的,因此爲了保障回調函數能在指定時間內執行,Chrome 中除了正常使用的消息隊列以外,還有另一個消息隊列,這個隊列中維護了須要延遲執行的任務列表。setTimeout 任務就被添加到延遲執行隊列中。

  3. C++ 模擬實現延遲隊列

    DelayedIncomingQueue delayed_incoming_queue;  // 源碼中延遲隊列的定義
    
    // 模擬實現一個回調任務
    struct DelayTask {
        int64 id;
        CallBackFunction cbf;
        int start_time;
        int delay_time;
    };
    DelayTask timerTask;
    timerTask.cbf = showName;
    timerTask.start_time = getCurrentTime(); // 獲取當前時間
    timerTask.delay_time = 200;  // 設置延遲時間
    
    delayed_incoming_queue.push(timerTask);   // 將回調任務添加到延遲執行隊列中
    複製代碼
  4. 完善事件循環的代碼

    void ProcessDelayTask() {
        // 從delayed_incoming_queue中取出已經到期的定時器任務
        // 依次執行這些任務
    }
    
    TaskQueue task_queue;
    void ProcessTask();  // 執行任務
    bool keep_running = true;
    void MainThread() {
        for(;;) {
            // 執行消息隊列中的任務
            Task task = task_queue.takeTask();
            ProcessTask(task);
            
            // 執行延遲隊列中的任務
            ProcessDelayTask();
            
            if(!keep_running)  // 若是設置了退出標誌,那麼直接退出線程循環
                break;
        }
    }
    複製代碼

    每處理完消息隊列中的一個任務以後,就開始執行延遲隊列中到期的任務。等到期的任務執行完成以後,再繼續下一個循環過程。這裏的延遲隊列其實是一個 hashmap 結構。

  5. 取消定時器

    調用clearTimeout函數,傳入須要取消的定時器的 ID。瀏覽器內部實現取消定時器的操做是直接從延遲隊列delayed_incoming_queue中經過 ID 查找到對應的任務,而後將其從隊列中刪除。

  6. 使用 setTimeout 的一些注意事項

    (1) 若是當前任務執行太久,會影響延遲到期定時器任務的執行。

    (2) 若是 setTimeout 存在嵌套調用,那麼系統會設置最短期間隔爲 4 毫秒。

    (3) 未激活的頁面,setTimeout 執行最小間隔是 1000 毫秒,目的是爲了優化後臺頁面的加載損耗以及下降耗電量。

    (4) 延遲執行時間有最大值。Chrome、Safari、Firefox 都是以32 個 bit來存儲延時值的,延遲值大於 32 bit 能夠存放的最大數字時會溢出,致使定時器會被當即執行。

    (5) 使用 setTimeout 設置的回調函數中的 this 不符合直覺。

    var name = 1;
    var myObj = {
        name: 2,
        showName: function() {
            console.log(this.name);
        }
    }
    setTimeout(myObj.showName, 1000);   // 延遲1秒執行,結果是:1
    setTimeout(myObj.showName(), 1000);   // 當即執行,結果是:2
    setTimeout(function() {
        myObj.showName();         // 延遲1秒執行,結果是:2
    }, 1000);
    setTimeout(() => {
        myObj.showName();         // 延遲1秒執行,結果是:2
    }, 1000);
    setTimeout(myObj.showName.bind(myObj), 1000);   // 延遲1秒執行,結果是2 
    複製代碼
  7. requestAnimationFrame實現的動畫效果比setTimeout好的緣由是什麼?

    (1) setTimeout 經過設置一個間隔時間來不斷改變圖像的位置,從而達到動畫效果。可是用 setTimeout 實現的動畫可能會出現卡頓、抖動的現象。有兩個緣由:

    • setTimeout 的執行時間不是肯定的。在 JavaScript 中,setTimeout 任務被放進了延遲執行隊列,主線程上的任務執行完後纔會檢查該隊列中的任務是否須要開始執行,因此 setTimeout 的實際執行時間通常比其設定的時間晚一些。
    • 不一樣設備的屏幕刷新頻率可能會不一樣,而 setTimeout 只能設置一個固定的時間間隔,這個時間不必定和屏幕的刷新時間相同。

    這兩種狀況致使 setTimeout 的執行步調和屏幕的刷新步調不一致,從而引發丟幀現象,致使動畫卡頓。

    (2) requestAnimationFrame 是由系統來決定回調函數的執行時機的,它能保證回調函數在屏幕每一次的刷新間隔中只被執行一次,這樣就不會引發丟幀現象。

    除此以外,requestAnimationFrame 還有CPU節能函數節流的優點。由於頁面被隱藏或最小化時,requestAnimationFrame 會中止渲染,但 setTimeout 還會在後臺繼續執行動畫任務。在高頻率事件如 resize 和 scroll 中,requestAnimationFrame 能夠保證在每一個刷新間隔內,函數只被執行一次。

3、XMLHttpRequest

  1. 系統調用棧

    消息隊列和主線程循環機制保證了頁面有條不紊地執行。當循環系統在執行一個任務的時候,都要爲這個任務維護一個系統調用棧。這個系統調用棧相似於 JavaScript 的調用棧,只不過是用 C++ 語言來維護的。能夠經過 Chrome 開發者工具的 Performance 抓取核心調用信息。

  2. 什麼是回調函數?

    將一個函數做爲參數傳遞給另一個函數,做爲參數的這個函數就是回調函數

    • 回調函數在主函數返回以前執行的回調過程稱爲同步回調,回調函數在當前主函數的上下文中執行。
    • 回調函數在主函數外部執行的過程稱爲異步回調,通常有兩種方式:
      • 第一種是把異步函數作成一個任務,添加到消息隊列尾部;
      • 第二種是把異步函數添加到微任務隊列中,微任務在當前任務的末尾處執行。
  3. XMLHttpRequest 運做機制

    (1) 建立 XMLHttpRequest 對象;

    (2) 爲 xhr 對象註冊回調函數:ontimeoutonerroronreadystatechange

    (3) 打開請求:open()

    (4) 配置基礎的請求信息;

    (5) 發起請求

    • 渲染進程將請求發送給網絡進程;
    • 網絡進程下載請求資源,接收到數據以後,用進程間通訊 IPC 通知渲染進程;
    • 渲染進程將 xhr 的回調函數封裝成任務並添加到消息隊列中;
    • 主線程循環系統執行到該任務的時候,根據相關的狀態來調用對應的狀態函數。
  4. XMLHttpRequest 示例代碼

    function getDataByXhr(url) {
        // 1.新建 XMLHttpRequest 請求對象
        let xhr = new XMLHttpRequest()
        
        // 2.註冊事件回調函數
        xhr.onreadystatechange = function() {
            switch (xhr.readyState) {
                case 0:    // 請求未初始化。還沒有調用open()方法
                    console.log('請求未初始化');
                    break;
                case 1:    // 請求已啓動。已經調用open()方法,但還沒有調用send()方法
                    console.log('OPENED');
                    break;
                case 2:    // 請求已發送。已經調用send()方法,但還沒有接收到響應
                    console.log('HEADERS_RECEIVED');
                    break;
                case 3:    // 正在接收。已經接收到部分響應數據
                    console.log('LOADING');
                    break;
                case 4:    // 請求完成。已經接收到所有響應數據
                    if (xhr.status == 200 || xhr.status == 304) {
                        console.log(xhr.responseText);
                    }
                    console.log('DONE')
                    break;
            }
        }
        
        xhr.ontimeout = function(e) { console.log('timeout', e) }
        xhr.onerror = function(e) { console.log('error', e) }
        
        // 3.打開請求
        xhr.open('GET', url, true);  // open()方法的第三個參數設置爲true,表示異步請求
        
        // 4.配置參數
        xhr.timeout = 3000   // 設置請求的超時時間
        xhr.responseType = 'json'   // 設置響應返回的數據格式
        // xhr.setRequestHeader()
        
        // 5.發送請求
        xhr.send();
    }
    複製代碼
  5. XMLHttpRequest 使用過程當中可能遇到的問題

    (1) 跨域問題

    (2) HTTPS 混合內容的問題

    • HTTPS 混合內容是 HTTPS 頁面中包含了不符合 HTTPS 安全要求的內容。好比 HTTP 資源,包括經過 HTTP 加載的圖像、視頻、樣式表、腳本等。
    • 經過 HTML 文件加載混合資源時,瀏覽器會針對 HTTPS 混合內容顯示警告,但大部分類型依然能夠加載。而使用 XMLHttpRequest 請求時,瀏覽器會認爲這種請求多是攻擊者發起的,會阻止此類請求。
  6. setTimeout 和 XMLHttpRequest 工做機制的區別

    • setTimeout 直接將延遲任務添加到延遲隊列中。
    • XMLHttpRequest 發起請求時,由瀏覽器的其餘進程或線程去執行,而後再將執行結果利用 IPC 的方式通知渲染進程,以後渲染進程再將對應的消息添加到消息隊列中。

4、宏任務和微任務

  1. 關於消息隊列

    WHATWG 規範定義了在主線程的循環系統中,能夠有多個消息隊列,好比鼠標事件隊列,IO 完成消息隊列,渲染任務隊列,而且能夠給這些消息隊列排優先級。但瀏覽器目前只實現了消息隊列和延遲執行隊列。

  2. 什麼是宏任務?

    消息隊列中的任務稱爲宏任務。

  3. 宏任務的執行過程

    • 先從多個消息隊列中選出一個最老的任務,這個任務稱爲 oldestTask;
    • 而後循環系統記錄任務開始執行的時間,並把這個 oldestTask 設置爲當前正在執行的任務;
    • 當任務執行完成以後,刪除當前正在執行的任務,並從對應的消息隊列中刪除這個 oldestTask;
    • 最後統計執行完成的時長等信息。
  4. 爲何須要微任務?

    由於 JavaScript 代碼不能掌控宏任務添加到隊列中的位置,難以控制開始執行任務的時間。對時間精度要求較高的需求,宏任務難以勝任,因此須要微任務。

  5. 什麼是微任務?

    微任務是一個須要異步執行的函數,執行時機是在主函數執行結束以後、當前宏任務結束以前。

  6. 微任務是如何產生的?

    產生微任務有兩種方式。

    • 第一種方式是使用 MutationObserver 監控某個 DOM 節點,而後再經過 JavaScript 來修改這個節點,或者爲這個節點添加、刪除部分子節點,當 DOM 節點發生變化時,就會產生 DOM 變化記錄的微任務。
    • 第二種方式是使用 Promise,當調用Promise.resolve()或者Promise.reject()的時候,也會產生微任務。
  7. 執行微任務隊列的時機

    • 一般狀況下,在當前宏任務中的 JavaScript 快執行完成時,也就是在 JavaScript 引擎準備退出全局執行上下文並清空調用棧的時候,JavaScript 引擎會檢查全局執行上下文中的微任務隊列,而後按照順序執行隊列中的微任務。(WHATWG 把執行微任務的時間點稱爲檢查點)
    • 若是在執行微任務的過程當中,產生了新的微任務,一樣會將該任務添加到微任務隊列中,V8 引擎一直循環執行微任務隊列中的任務,直到隊列爲空纔算執行結束。也就是說在執行微任務過程當中產生的新的微任務並不會推遲到下個宏任務中執行,而是在當前的宏任務中繼續執行。
  8. 微任務和宏任務

    • 微任務和宏任務是綁定的,每一個宏任務在執行時,會建立本身的微任務隊列。
    • 微任務的執行時長會影響到當前宏任務的時長。
    • 在一個宏任務中分別建立一個用於回調的宏任務和微任務,不管什麼狀況下,微任務都早於宏任務執行。
  9. Mutation Event 和 MutationObserver 監聽 DOM 變化

    (1) Mutation Event 採用觀察者模式監聽 DOM 變化。當 DOM 有變更時就馬上觸發相應的事件,這種方式屬於同步回調。可是這種實時性形成了嚴重的性能問題。

    (2) MutationObserver 將事件的響應函數改爲異步調用,不是在每次 DOM 變化都觸發異步調用,而是等屢次 DOM 變化後,一次觸發異步調用。每次 DOM 節點發生變化的時候,渲染引擎將變化記錄封裝成微任務,並將微任務添加進當前的微任務隊列中。當執行到檢查點的時候,V8 引擎就會按順序執行這些微任務。

    MutationObserver 經過異步調用和減小觸發次數解決同步操做的性能問題,經過微任務解決實時性問題


參考:

  1. 【極客時間】瀏覽器原理,李兵。
  2. 【CSDN】深刻理解 requestAnimationFrame: blog.csdn.net/vhwfr2u02q/…
相關文章
相關標籤/搜索