什麼是瀏覽器的Event Loop

Event Loop是什麼

由於js設計之初,多線程的執行模式還不流行,因此一直覺得,js都是單線程執行的。可是js擁有異步執行的能力,這依賴於事件循環(Event Loop)的執行模式。咱們將經過js在瀏覽器中的執行來研究一下該模式。javascript

其中涉及到一些概念,咱們先簡單研究一下,以便後續更好地瞭解。java

進程和線程

參考阮一峯的解釋,將整個CPU比喻爲一座工廠,進程就是其中的車間,車間中的須要完成的工序就是線程。一個工廠能夠有多個車間,每一個車間有一個或者多個工序,可是必須 按照順序執行,這就是單線程的概念。也是瀏覽器事件執行的基礎。ajax

瀏覽器渲染過程

瀏覽器是一個多進程應用,每個窗口就是一個進程,其中包含如下線程:promise

  • GUI渲染線程

負責渲染頁面,佈局和繪製瀏覽器

頁面須要重繪和迴流時,該線程就會執行網絡

與js引擎線程互斥,防止渲染結果不可預期數據結構

  • JS引擎線程

負責處理解析和執行javascript腳本程序多線程

只有一個JS引擎線程(單線程)異步

與GUI渲染線程互斥,防止渲染結果不可預期async

  • 事件觸發線程

用來控制事件循環(鼠標點擊、setTimeout、ajax等)

當事件知足觸發條件時,將事件放入到JS引擎所在的執行隊列中

  • 定時觸發器線程

setInterval與setTimeout所在的線程

定時任務並非由JS引擎計時的,是由定時觸發線程來計時的

計時完畢後,通知事件觸發線程

  • 異步http請求線程

瀏覽器有一個單獨的線程用於處理AJAX請求

當請求完成時,如有回調函數,通知事件觸發線程

各個進程之間的關係

  • 同步任務都在js引擎線程上完成,當前的任務都存儲在執行棧中;

  • js引擎線程執行到setTimeout/setInterval的時候,通知定時觸發器線程,間隔必定時間,觸發回調函數;

  • 定時觸發器線程在接收到這個消息後,會在等待的時間後,將回調事件放入到由事件觸發線程所管理的事件隊列(事件隊列分爲宏任務隊列微任務隊列)中;

  • js引擎線程執行到XHR/fetch時,通知 異步http請求線程,發送一個網絡請求;

  • 異步http請求線程在請求成功後,將回調事件放入到由事件觸發線程事件隊列中;

  • 若是JS引擎線程中的執行棧沒有任務了,JS引擎線程會詢問事件觸發線程,在 事件隊列中是否有待執行的回調函數,若是有就會加入到執行棧中交給JS引擎線程執行;

  • JS引擎線程空閒以後,GUI渲染線程開始工做

各個進程的關係

總結:

  • JS 是能夠操做 DOM 的, 所以瀏覽器設定 GUI渲染線程和 JS引擎線程爲互斥關係;

  • setTimeout/setIntervalXHR/fetch代碼執行時, 自己是同步任務,而其中的回調函數纔是異步任務

  • JS引擎線程只執行執行棧中的事件

  • 執行棧中的代碼執行完畢,就會讀取事件隊列中的事件

  • 事件隊列中的回調事件,是由各自線程插入到事件隊列中的

  • 如此循環

js如何異步執行

瞭解了瀏覽器多線程之間的關聯以後,咱們開始探究,js是如何依賴Event Loop,進行異步操做的。

執行棧和事件隊列

在分析多線程之間的關係時,咱們提到了兩個概念,執行棧執行隊列

執行棧

棧,是一種數據結構,具備先進後出的原則。JS 中的執行棧就具備這樣的結構,當引擎第一次遇到 JS 代碼時,會產生一個全局執行上下文並壓入執行棧,每遇到一個函數調用,就會往棧中壓入一個新的上下文。引擎執行棧頂的函數,執行完畢,彈出當前執行上下文

事件隊列

事件隊列是一個存儲着 異步任務 的隊列,按照先進先出的原則執行。事件隊列每次僅執行一個任務。當執行棧爲空時,JS 引擎便檢查事件隊列,若是事件隊列不爲空的話,事件隊列便將第一個任務壓入執行棧中運行。

宏任務和微任務

異步任務又分爲宏任務跟微任務、他們之間的區別主要是執行順序的不一樣。

宏任務(macrotask)

也叫tasks,一些異步任務的回調會依次進入macro task queue,等待後續被調用,這些異步任務包括:

  • 包括總體代碼script
  • setTimeout
  • setInterval
  • requestAnimationFrame

微任務(microtask)

也叫jobs,另外一些異步任務的回調會依次進入micro task queue,等待後續被調用,這些異步任務包括:

  • Promise
  • MutationObserver

理解Event Loop

示例

下面上一道很經典的題目:

console.log('1');
    setTimeout(()=>{
        console.log('2');
    },100);
    setTimeout(()=>{
        console.log('3');
    },0);
    console.log('4');
複製代碼

沒有研究event loop以前,答案極可能覺得是1 3 4 2,可是實際答案是1 4 3 2。其中的原理下面來分析一下。

異步任務執行的時候,有這樣一個順序:

  1. 執行全局Script代碼,若是碰到異步任務,將該任務放入微任務隊列中
  2. 全局Script執行完,執行棧清空
  3. 從微隊列microtask queue中取出位於隊首的回調任務,放入調用棧Stack中執行
  4. 一個微任務執行完畢以後,再從微任務隊列中取出一個任務放入執行棧執行,若微任務中還有微任務,則放入當前微任務隊列末尾
  5. 微任務隊列爲空,執行棧也爲空,此時從宏任務隊列取出一個任務執行,若是其中有微任務,放入微任務隊列
  6. 重複執行3-5步驟...
  7. 重複執行3-5步驟...

*注:

  1. 宏任務隊列一次只從隊列中取一個任務執行,執行完後就去執行微任務隊列中的任務;
  2. 微任務隊列中全部的任務都會被依次取出來執行,直到隊列爲空;
  3. GUI渲染線程在微任務執行完,執行棧爲空,下一個宏任務執行以前執行,。

以上就是瀏覽器事件循環——event loop。

理解了異步任務的執行順序以後,再來回顧上面這道題:

console.log('1');
    setTimeout(()=>{
        console.log('2');
    },100);
    setTimeout(()=>{
        console.log('3');
    },0);
    console.log('4');
複製代碼
  1. 執行整個script,console.log('1')是同步任務,setTimeout是宏任務,js引擎線程通知事件觸發線程,在定時n秒後存入宏任務隊列中,因此先存入console.log('3'),後存入console.log('2');
  2. 執行下一個同步任務console.log('4');
  3. 執行棧爲空,查詢微任務隊列也爲空,查詢宏任務隊列
  4. 根據先進先出原則,執行console.log('3');,後執行console.log('2');
  5. 輸出1 4 3 2;

實踐

再來2道題鞏固一下:

一.

setTimeout(() => {
      console.log('A');
    }, 0);
    var obj = {
      func: function() {
        setTimeout(function() {
          console.log('B');
        }, 0);
        return new Promise(function(resolve) {
          console.log('C');
          resolve();
        });
      },
    };
    obj.func().then(function() {
      console.log('D');
    });
    console.log('E');
複製代碼
  1. 第一個setTimeout放到宏任務隊列,此時宏任務隊列爲['A'];
  2. 接着執行objfunc方法,將setTimeout放到宏任務隊列,此時宏任務隊列爲['A', 'B']
  3. 函數返回一個Promise,由於這是一個同步操做,因此先打印出'C';
  4. 接着將then放到微任務隊列,此時微任務隊列爲 ['D'];
  5. 接着執行同步任務console.log('E');,打印出 'E';
  6. 由於微任務優先執行,因此先輸出 'D';
  7. 最後依次輸出 ['A', 'B'];
  8. 輸出結果:C E D A B

二.

async function async1() {
      console.log('async1 start');
      await async2();
      console.log('async1 end');
    }
    async function async2() {
      console.log('async2');
    }
    console.log('script start');
    setTimeout(function() {
      console.log('setTimeout');
    }, 0);
    async1();
    new Promise(function(resolve) {
      console.log('promise1');
      resolve();
    }).then(function() {
      console.log('promise2');
    });
    console.log('script end');
複製代碼

await前面的代碼是同步的,調用此函數時會直接執行;而await a(); 這句能夠被轉換成 Promise.resolve(a()); await 後面的代碼 則會被放到 Promise.then() 方法裏。所以上面的代碼能夠被轉換成以下形式:

function async1() {
      console.log('async1 start'); // 2
    
      Promise.resolve(async2()).then(() => {
        console.log('async1 end'); // 6
      });
    }
    
    function async2() {
      console.log('async2'); // 3
    }
    
    console.log('script start'); // 1
    
    setTimeout(function() {
      console.log('settimeout'); // 8
    }, 0);
    
    async1();
    
    new Promise(function(resolve) {
      console.log('promise1'); // 4
      resolve();
    }).then(function() {
      console.log('promise2'); // 7
    });
    console.log('script end'); // 5
複製代碼
  1. 首先打印出script start
  2. 接着將settimeout添加到宏任務隊列,此時宏任務隊列爲['settimeout']
  3. 而後執行函數async1,先打印出async1 start,又由於Promise.resolve(async2()) 是同步任務,因此打印出async2,接着將async1 end 添加到微任務隊列,此時微任務隊列爲['async1 end']
  4. 接着打印出promise1,將promise2 添加到微任務隊列,此時微任務隊列爲['async1 end', promise2]
  5. 打印出script end
  6. 由於微任務優先級高於宏任務,因此先依次打印出 async1 endpromise2
  7. 最後打印出宏任務settimeout
相關文章
相關標籤/搜索