1. 程式人生 > >[瀏覽器事件循環] javaScript事件循環 EventLoop

[瀏覽器事件循環] javaScript事件循環 EventLoop

發現 按順序 總結 維數 完成後 js調用 observer 特殊 基本上

前言

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 腳本。

和 GUI 渲染線程互斥,JS 運行耗時過長就會導致頁面阻塞。

【事件觸發線程】
當事件符合觸發條件被觸發時,該線程會把對應的事件回調函數添加到任務隊列的隊尾,等待 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