EventLoop其實如此簡單

瀏覽器的EventLoop

瀏覽器機制:

瀏覽器的主要組件

瀏覽器的主要組件包括:

  1. 用戶界面 - 包括地址欄、前進/後退按鈕、書籤菜單等。除了瀏覽器主窗口顯示的你請求的頁面外,其餘顯示的各個部分都屬於用戶界。
  2. 瀏覽器引擎 - 在用戶界面和渲染引擎之間傳送指令。
  3. 渲染引擎 - 負責顯示請求的內容。若是請求的內容是 HTML,它就負責解析 HTML 和 CSS 內容,並將解析後的內容顯示在屏幕上。
  4. 網絡 - 用於網絡調用,好比 HTTP 請求。其接口與平臺無關,併爲全部平臺提供底層實現。
  5. 用戶界面後端 - 用於繪製基本的窗口小部件,好比組合框和窗口。其公開了與平臺無關的通用接口,而在底層使用操做系統的用戶界面方法。
  6. JavaScript 解釋器。用於解析和執行 JavaScript 代碼,好比chrome的javascript解釋器是V8。
  7. 數據存儲。這是持久層。瀏覽器須要在硬盤上保存各類數據,例如Cookie。新的HTML規範(HTML5)定義了「網絡數據庫」,這是一個完整(可是輕便)的瀏覽器內數據庫。

瀏覽器渲染流程:

瀏覽器渲染流程

  1. render:渲染引擎解析HTML文檔,並將文檔中的標籤轉化爲dom節點樹,即」內容樹」。同時,它也會解析外部CSS文件以及syle標籤中的樣式數據。這些樣式信息連同HTML中的」可見內容」一道,被用於構建另外一棵樹——」渲染樹(Render樹)」。渲染樹由一些帶有視覺屬性(如顏色、大小等)的矩形組成,這些矩形將按照正確的順序顯示在頻幕上。
  2. layout:渲染樹構建完畢以後,將會進入」佈局」處理階段,即爲每個節點分配一個屏幕座標。
  3. painting:即遍歷render樹,並使用UI後端層繪製每一個節點。

瀏覽器的單線程和任務隊列:

瀏覽器EventLoop原理圖

  1. 瀏覽器是單線程,Js的主要用途是與用戶互動以及操做DOM。這決定了它只能是單線程,不然會帶來很複雜的同步問題。好比,假定Js同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器不知道以哪一個線程爲準,會產生混亂。
  2. 瀏覽器是單線程,並表示只有一個線程而是隻擁有一個主線程js解析和ui渲染,其餘異步任務有其單獨的線程,例如:DOM事件、ajax調用、setTimeout
  3. dom事件、ajax調用、定時器等異步任務會開單獨的線程,它們會往異步隊列中存放回調函數,不阻塞主線程的運行
  4. 主線程執行完成以後會從異步隊列中取出回調函數運行
  5. 異步隊列中存在宏任務隊列(task)和微任務(microtask)隊列
  6. 宏任務:script(內嵌和外鏈)、setImmediate、MessageChannel、setTimeout,微任務:Promise.then、MutationObserver

瀏覽器EventLoop過程:

宏任務處理:

  1. 選擇當前要執行的任務隊列task,選擇一個最早進入任務隊列的任務,若是沒有任務能夠選擇,則會跳轉至microtask的執行步驟。 將事件循環的當前運行任務設置爲已選擇的任務。
  2. 運行宏任務。
  3. 將事件循環的當前運行任務設置爲null。
  4. 將運行完的任務從任務隊列task中移除。 microtasks步驟:進入microtask檢查點(performing a microtask checkpoint )。
  5. 更新界面渲染。
  6. 返回第一步。

微任務處理(microtask的執行步驟):

  1. 設置進入microtask檢查點的標誌爲true。
  2. 當事件循環的微任務隊列不爲空時:選擇一個最早進入microtask隊列的microtask;設置事件循環的當前運行任務爲已選擇的microtask
  3. 運行microtask;設置事件循環的當前運行任務爲null;將運行結束的microtask從microtask隊列中移除。
  4. 對於相應事件循環的每一個環境設置對象(environment settings object),通知它們哪些promise爲rejected。
  5. 清理indexedDB的事務。
  6. 設置進入microtask檢查點的標誌爲false。

上面的過程能夠總結爲:javascript

  1. 查看task中是否存在任務,若是存在則執行宏任務,執行完畢將其從任務隊列task中刪除
  2. 若是task中不存在任務,查看microtask是否存在任務,存在執行微任務,執行完畢將其從microtask;不然執行下一輪循環從新查看task

代碼實例分析:

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');

//script start
//script end
//promise1
//promise2
//setTimeout
複製代碼
  1. 開始時,task中只有script,則script中全部函數放入stack中按順序執行執行。
  2. 執行到setTimeout,script執行完後會將回調函數放入task隊列中,將在下一個事件循環中執行。
  3. 執行到Promise,Promise屬於microtask,因此會將第一個.then()放入microtask隊列。
  4. 當script代碼執行完畢後,此時task爲空。開始檢查microtask隊列,執行.then()的回調函數輸出'promise1',因爲.then()返回的依然是promise,因此第二個.then()會放入microtask隊列繼續執行,輸出'promise2'。
  5. microtask隊列爲空了,進入下一個事件循環,檢查task隊列發現了setTimeout的回調函數,當即執行回調函數輸出'setTimeout',異步代碼執行完畢。

node的EventLoop

node中的處理流程:

node流程

  1. v8引擎從上到下解析node主程序
  2. 當調用fs,buffer等nodeAPI時會調用底層的libuv函數庫,利用多線程+事件池實現同步非阻塞先將回調放在異步隊列EventQueue中
  3. 當調用底層的libuv庫的方法成功後會找到EventQueue中相應的回調函數執行,並將結果返回。

node中的EventLoop和瀏覽器中的EventLoop存在一些差異,node是經過多線程來實現的,能夠同時處理多個任務。當其中一個任務完成時,相應的callback被插入到輪詢隊列中,最終被執行。java

node中的任務隊列:

  1. timers:執行setTimeout()和setInterval安排的回調
  2. I/O callbacks: 執行幾乎全部異常的close回調,由timer和setImmediate執行的回調。
  3. idle,prepare: 只用於內部
  4. poll : 獲取新的I/O事件,node在該階段會適當的阻塞
  5. check : setImmediate的回調被調用
  6. close callbacks: e.g socket.on(‘close’,…);

node中EventLoop流程:

瀏覽器渲染流程

  1. timers,定時器階段: 執行定時任務(setTimeOut(), setInterval())
  2. poll 輪詢階段:
    • 處理到期的定時器任務,而後(由於最開始階段隊列爲空,一旦隊列爲空,就會檢查是否有到期的定時器任務)
    • 處理隊列任務,直到隊列空,或達到上限
    • 若是隊列爲空:若是setImmediate,終止輪詢階段,進入檢查階段執行。若是沒setImmediate,查看有沒有定時器任務到期,有的話就到timers階段,執行回調函數.
  3. check 檢查階段:輪詢階段空閒,且有setImmediate的時候,進入檢查階段

上述的五個階段都是按照先進先出的規則執行回調函數。按順序執行每一個階段的回調函數隊列,直至隊列爲空或是該階段執行的回調函數達到該階段所容許一次執行回調函數的最大限制後,纔會將操做權移交給下一階段,不然的話不會進入下一個階段。node

區分setImmediate()與setTimeout()

從上面的poll和check階段的邏輯,咱們能夠看出setImmediate和setTimeout、setInterval都是在poll階段執行完當前的I/O隊列中相應的回調函數後觸發的。可是這兩個函數倒是由不一樣的路徑觸發的:ajax

  1. setImmediate函數,是在當前的pollqueue對列執行後爲空或是執行的數目達到上限後,eventloop直接調入check階段執行setImmediate函數。
  2. setTimeout、setInterval則是在當前的pollqueue對列執行後爲空或是執行的數目達到上限後,eventloop去timers檢查是否存在已經到期的定時器,若是存在直接執行相應的回調函數。
  3. 程序中既有setTimeout和setImmediate時,在非I/O循環(主模塊)中,順序不固定;在I/O循環中setImmdiate回調老是先執行
//在非I/O循環(主模塊)中,順序不固定
setTimeout(function timeout() {
  console.log('timeout');
}, 0);

setImmediate(function immediate() {
  console.log('immediate');
});
複製代碼
// 在I/O循環中setImmdiate回調老是先執行
const fs = require('fs');

fs.readFile(__filename, () => {
  setTimeout(() => {
    console.log('timeout');
  }, 0);
  setImmediate(() => {
    console.log('immediate');
  });
});
複製代碼

區分process.nextTick()與setImmediate()

  1. process.nextTick() 函數是無論當前正在eventloop的哪一個階段,在當前階段執行完畢後,跳入下個階段前的瞬間執行;setImmediate() 函數是在poll階段後進去check階段事執行
  2. process.nextTick() 函數的應用
//容許線程在進入event loop下一個階段前作一些關於處理異常、清理一些無用或無關的資源。
function apiCall(arg, callback) {
  if (typeof arg !== 'string')
    return process.nextTick(callback,new TypeError('argument should be string'));
}
複製代碼
//在進入下個event loop階段前,而且回調函數尚未釋放回調權限時執行一些相關操做。
//在MyEmitter構造函數實例化前註冊「event」事件,這樣就能夠保證明例化後的函數能夠監聽「event」事件。
const EventEmitter = require('events');
const util = require('util');

function MyEmitter() {
  EventEmitter.call(this);

  // use nextTick to emit the event once a handler is assigned
  process.nextTick(function() {
    this.emit('event');
  }.bind(this));
}
util.inherits(MyEmitter, EventEmitter);

const myEmitter = new MyEmitter();
myEmitter.on('event', function() {
  console.log('an event occurred!');
});
複製代碼

結語:

以上就是關於EventLoop的介紹,若是有錯誤歡迎指正,本文參考:chrome

  1. 什麼是瀏覽器事件循環(EventLoop)
  2. 不要混淆nodejs和瀏覽器中的event loop
  3. 快速掌握Nodejs系列之—Events模塊
  4. 深刻理解nodejs Event loop
  5. Nodejs 解讀event loop的事件處理機制
相關文章
相關標籤/搜索