我認為要理解事件迴圈,首先要理解什麼是程序,什麼是執行緒。
程序和執行緒
什麼是程序?
百科解釋: 程序(Process) 是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。 在當代面向執行緒設計的計算機結構中,程序是執行緒的容器。程式是指令、資料及其組織形式的描述,程序是程式的實體。是計算機中的程式關於某資料集合上的一次執行活動,是系統進行資源分配和排程的基本單位,是作業系統結構的基礎。程式是指令、資料及其組織形式的描述,程序是程式的實體。
說實話這種解釋我是看不懂。一番查閱後,找了一個很不錯的介紹:
程式執行需要有它自己專屬的記憶體空間,可以把這塊記憶體空間簡單的理解為程序
“專屬的記憶體空間”就可以理解為獨立。
可以透過瀏覽器f12,檢視工作管理員來看下開啟了哪些程序
每個應用至少有一個程序,程序之間相互獨立,即使要通訊,也需要雙方同意。
什麼是執行緒?
百科解釋: 執行緒(thread) 是作業系統能夠進行運算排程的最小單位。它被包含在程序之中,是程序中的實際運作單位。一條執行緒指的是程序中一個單一順序的控制流,一個程序中可以併發多個執行緒,每條執行緒並行執行不同的任務。
執行緒是最小的執行單元,而程序由至少一個執行緒組成。啟動一個程式的時候,作業系統會為該程式建立一塊記憶體,用來存放程式碼、執行中的資料和一個執行任務的主執行緒,我們把這樣的一個執行環境叫程序。說的在透徹一點,執行程式碼的「人」稱之為「執行緒」。
一個程序可以包含多個執行緒,但是一個程序至少要有一個執行緒,所以在程序開啟後會自動建立一個執行緒來執行程式碼,該執行緒稱之為主執行緒。
瀏覽器包含哪些程序和執行緒
瀏覽器程序。主要負責介面顯示、使用者互動、子程序管理,同時提供儲存等功能
渲染程序。核心任務是將 HTML、CSS 和 JavaScript 轉換為使用者可以與之互動的網頁,排版引擎 Blink 和 JavaScript 引擎 V8 都是執行在該程序中,預設情況下,Chrome 會為每個 Tab 標籤建立一個渲染程序。出於安全考慮,渲染程序都是執行在沙箱模式下。
網路程序。主要負責頁面的網路資源載入,之前是作為一個模組執行在瀏覽器程序裡面的,直至最近才獨立出來,成為一個單獨的程序。
我們這裏主要來看渲染程序,渲染程序啟動後,會開啟一個渲染主執行緒,主執行緒主要負責執行HTML、CSS、JS程式碼。
渲染主執行緒
每個渲染程序都有一個主執行緒,並且主執行緒非常繁忙,既要處理 DOM,又要計算樣式,還要處理佈局,同時還需要處理 JavaScript 任務以及各種輸入事件。那麼主執行緒如何排程任務呢?
排隊
將這些任務按照順序加入訊息佇列中,並且按照“先進先出”一個個執行。
我們來分析一下執行步驟:
渲染主執行緒會進入到一個無線迴圈中
如果新增一個任務,那麼則將新增的任務新增到訊息佇列的末尾
每一次迴圈都會檢查訊息佇列中是否有任務存在,如果存在,那麼渲染主執行緒會從訊息佇列的頭部中讀取任務,並執行任務,執行完畢後,則進入下一次迴圈;如果不存在,則主執行緒進入休眠狀態。
這裏新增任務並不是只有渲染執行緒的任務,其他所有執行緒(包括其他程序中的執行緒)可以隨時向訊息佇列中新增任務。在新增新任務的時候,如果主執行緒是處於休眠狀態,那麼則將主執行緒喚醒以繼續迴圈讀取並執行任務。
非同步
我們知道,所有的任務都是在單執行緒執行的,而且每次只能執行一個任務,那麼其他的任務就都處於一個等待的狀態。而有些任務在程式碼執行的過程中,是無法立即處理的,那麼這樣就會導致一個很嚴重的問題。如果瀏覽器主執行緒等待這些無法立即處理的任務的時機達到,就會導致主執行緒長期處於一個等待的狀態,也就是我們常說的"阻塞",就會導致瀏覽器卡死。
從圖中可以看到,當計時開始的時候,渲染主執行緒一直處於等待的狀態。
由於渲染主執行緒承擔著極其重要的工作,所以絕對不能阻塞,瀏覽器選擇了非同步來解決這個問題
使用非同步,當計時開始的時候,渲染主執行緒將任務交給計時執行緒去處理,隨後自身立即結束任務的執行,轉而執行後續程式碼,當計時執行緒完成時,將事先傳遞的回撥函式包裝成任務,加入到訊息佇列的末尾排隊,等待主執行緒排程執行。
在這種非同步模式下,瀏覽器永不阻塞,從而最大限度的保證了單執行緒的流暢執行。
優先順序
訊息佇列是“先進先出”的特性,那麼放入佇列中的任務,要等待前面的任務執行完畢之後,才能被執行。但是如果有的任務比較著急執行呢?比如我們常說的微任務比宏任務先執行。這個是因為任務有優先順序嗎?
其實並不是,任務沒有優先順序,任務就是需要排隊,在佇列中先進先出,但是訊息佇列有優先順序。
根據w3c最新解釋 html.spec.whatwg.org/multipage/w…
每個任務都有一個任務型別,同一個型別的任務必須在一個佇列,不同型別的任務可以分屬於不同的佇列。 在一次事件迴圈中,瀏覽器可以根據實際情況從不同的佇列中取出任務執行。
瀏覽器必須準備好一個微佇列,微佇列中的任務優先所有其他任務執行
和我們關係比較大的,在chrome瀏覽器中,至少要包含以下佇列:
延時佇列 ——— 優先順序(中)
互動佇列 ——— 優先順序(高)
微佇列 ——— 優先順序(最高)
現代瀏覽器中,產生微任務有兩種方式。
第一種方式是使用 MutationObserver 監控某個 DOM 節點,然後再透過 JavaScript 來修改這個節點,或者為這個節點新增、刪除部分子節點,當 DOM 節點發生變化時,就會產生 DOM 變化記錄的微任務。
第二種方式是使用 Promise,當呼叫 Promise.resolve() 或者 Promise.reject() 的時候,也會產生微任務。
下面我們用一個題目來演示一下
Promise.resolve().then(() => { console.log('Promise1'); setTimeout(() => { console.log('setTimeout2'); }, 0); }); setTimeout(() => { console.log('setTimeout1'); Promise.resolve().then(() => { console.log('Promise2'); }); }, 0);
碰到這種題目,首先立刻畫個圖
首先渲染主執行緒開始執行js程式碼
當碰到
promise.resolve.then(fn)
,立即把一個函式新增到微佇列, 這裏我們稱為Promise1任務遇到setTimeout,然後交給計時執行緒計時,
在0s之後 將回調新增到延時佇列,這裏我們稱為setTimeout1任務 此時全域性js同步程式碼執行完畢。
而微佇列優先順序最高,則先執行Promise1任務,那麼則輸出Promise1,接著將遇到setTimeout,然後交給計時執行緒計時,在0s之後 將回調新增到延時佇列, 這裏我們稱為setTimeout2任務
Promise1任務執行完畢之後,微佇列中已經沒有任務了,只有延時佇列有任務,那麼先執行setTimeout1任務。那麼先輸出setTimeout1,然後碰到promise.resolve.then(fn)
,立即把一個函式新增到微佇列, 這裏我們稱為Promise2任務。
此時微佇列中有任務了,由於他的優先順序最高,那麼先執行Promise2任務,則輸出Promise2,Promise2任務執行完畢之後,微佇列中已經沒有任務了,只有延時佇列有任務,那麼先執行setTimeout2任務。那麼先輸出setTimeout2, 所以最後的結果為:Promise1、setTimeout1、Promise2、setTimeout2