[瀏覽器事件循環] javaScript事件循環 EventLoop
前言
Event Loop即事件循環,是指瀏覽器或Node的一種解決javaScript單線程運行時不會阻塞的一種機制,也就是我們經常使用異步的原理。
先熟悉基本概念
【堆Heap】
堆是一種數據結構,是利用完全二叉樹維護的一組數據,堆分為兩種,一種為最大堆,一種為最小堆,將根節點最大的堆叫做最大堆或大根堆,根節點最小的堆叫做最小堆或小根堆。
堆是線性數據結構,相當於一維數組,有唯一後繼。
【棧Stack】
棧在計算機科學中是限定僅在表尾進行插入或刪除操作的線性表。 棧是一種數據結構,它按照後進先出的原則存儲數據,先進入的數據被壓入棧底,最後的數據在棧頂,需要讀數據的時候從棧頂開始彈出數據。
【隊列Queue】
特殊之處在於它只允許在表的前端(front)進行刪除操作,而在表的後端(rear)進行插入操作,和棧一樣,隊列是一種操作受限制的線性表。
進行插入操作的端稱為隊尾,進行刪除操作的端稱為隊頭。 隊列中沒有元素時,稱為空隊列。
【進程】
進程是系統分配的獨立資源,是 CPU 資源分配的基本單位,進程是由一個或者多個線程組成的。
【線程】
線程是進程的執行流,是CPU調度和分派的基本單位,同個進程之中的多個線程之間是共享該進程的資源的。
把這些概念拿到瀏覽器中來說,當你打開一個 Tab 頁時,其實就是創建了一個進程,一個進程中可以有多個線程,比如渲染線程、JS 引擎線程、HTTP 請求線程等等。當你發起一個請求時,其實就是創建了一個線程,當請求結束後,該線程可能就會被銷毀。
瀏覽器內核
瀏覽器是多進程的,瀏覽器每一個 tab 標簽都代表一個獨立的進程(也不一定,因為多個空白 tab 標簽會合並成一個進程),瀏覽器內核(瀏覽器渲染進程)屬於瀏覽器多進程中的一種。
瀏覽器內核有多種線程在工作。
【GUI 渲染線程】
負責渲染頁面,解析 HTML,CSS 構成 DOM 樹等,當頁面重繪或者由於某種操作引起回流都會調起該線程。
和 JS 引擎線程是互斥的,當 JS 引擎線程在工作的時候,GUI 渲染線程會被掛起,GUI 更新被放入在 JS 任務隊列中,等待 JS 引擎線程空閑的時候繼續執行。
【JS 引擎線程】
單線程工作,負責解析運行 JavaScript 腳本。
【事件觸發線程】
當事件符合觸發條件被觸發時,該線程會把對應的事件回調函數添加到任務隊列的隊尾,等待 JS 引擎處理。
【定時器觸發線程】
瀏覽器定時計數器並不是由 JS 引擎計數的,阻塞會導致計時不準確。
開啟定時器觸發線程來計時並觸發計時,計時完成後會被添加到任務隊列中,等待 JS 引擎處理。
【http 請求線程】
http 請求的時候會開啟一條請求線程。
請求完成有結果了之後,將請求的回調函數添加到任務隊列中,等待 JS 引擎處理。
基礎知識我們基本了解了些必要的,下面我們開始介紹Event Loop
js中的任務分類
任務被分為兩種,一種宏任務(MacroTask)也叫Task,一種叫微任務(MicroTask) ?也叫jobs
【MacroTask(宏任務)】
類型:script全部代碼、setTimeout、setInterval、setImmediate、I/O、UI Rendering
【MicroTask(微任務)】
類型:Process.nextTick(Node獨有)、Promise 、MutationObserver
Event Loop
目前討論的兩種情況:瀏覽器的Event Loop 以及Node中的Event Loop
瀏覽器中的Event Loop
Javascript 有一個 main thread 主線程和 call-stack 調用棧(執行棧),所有的任務都會被放到調用棧等待主線程執行。
【JS調用棧】
JS調用棧采用的是後進先出的規則,當函數執行的時候,會被添加到棧的頂部,當執行棧執行完成後,就會從棧頂移出,直到棧內被清空。
【同步任務和異步任務】
Javascript單線程任務被分為同步任務和異步任務,同步任務會在調用棧中按照順序等待主線程依次執行,異步任務會在異步任務有了結果後,將註冊的回調函數放入任務隊列中等待主線程空閑的時候(調用棧被清空),被讀取到棧內等待主線程的執行。
瀏覽器進行事件循環工作方式
1、選擇當前要執行的任務隊列,選擇任務隊列中最先進入的任務,如果任務隊列為空即null,則執行跳轉到微任務(MicroTask)的執行步驟。
2、將事件循環中的任務設置為已選擇任務。
3、執行任務。
4、將事件循環中當前運行任務設置為null。
5、將已經運行完成的任務從任務隊列中刪除。
6、microtasks步驟:進入microtask檢查點。
7、更新界面渲染。
8、返回第一步。
【執行進入microtask檢查點時,瀏覽器會執行以下步驟:】
設置microtask檢查點標誌為true。
當事件循環microtask執行不為空時:選擇一個最先進入的microtask隊列的microtask,將事件循環的microtask設置為已選擇的microtask,運行microtask,將已經執行完成的microtask為null,移出microtask中的microtask。
清理IndexDB事務
設置進入microtask檢查點的標誌為false。
【重點】
總結以上規則為一條通俗好理解的:
1、順序執行先執行同步方法,碰到MacroTask直接執行,並且把回調函數放入MacroTask執行隊列中(下次事件循環執行);碰到microtask直接執行。把回調函數放入microtask執行隊列中(本次事件循環執行)
2、當同步任務執行完畢後,去執行微任務microtask。(microtask隊列清空)
3、由此進入下一輪事件循環:執行宏任務 MacroTask (setTimeout,setInterval,callback)
[總結]所有的異步都是為了按照一定的規則轉換為同步方式執行。
查看一個示例
console.log('script start');
setTimeout(function() {
console.log('setTimeout');
}, 0);
Promise.resolve().then(function() {
console.log('promise1');
}).then(function() {
console.log('promise2');
});
console.log('script end');
1、一開始task隊列中只有script,則script中所有函數放入函數執行棧執行,代碼按順序執行。
2、接著遇到了setTimeout,它的作用是0ms後將回調函數放入task隊列中,也就是說這個函數將在下一個事件循環中執行(註意這時候setTimeout執行完畢就返回了)。
3、接著遇到了Promise,按照前面所述Promise屬於microtask,所以第一個.then()會放入microtask隊列。
4、當所有script代碼執行完畢後,此時函數執行棧為空。
5、開始檢查microtask隊列,此時隊列不為空,執行.then()的回調函數輸出‘promise1‘,由於.then()返回的依然是promise,所以第二個.then()會放入microtask隊列繼續執行,輸出‘promise2‘。此時microtask隊列為空了,
6、進入下一個事件循環,檢查task隊列發現了setTimeout的回調函數,立即執行回調函數輸出‘setTimeout‘,代碼執行完畢。
小結
基本上能理解這個例子的話,對於瀏覽器的事件循環應該已經可以理解的差不多了。由於本篇文章涉及的知識點比較多,不易篇幅太長,至於node的事件循環方式則跟瀏覽器的實現方式不太一樣。所以後面會在總結一篇文章
[瀏覽器事件循環] javaScript事件循環 EventLoop