效能優化篇 - js事件迴圈機制(event loop)

熱情的劉大爺發表於2018-11-16

前言

之前在做前端的時候,可能注重的東西更偏向於業務層面的東西,切圖、實現互動、呼叫介面等一系列比較淺的技術層,隨著前端技術的不斷髮展,只掌握這些知識是不夠的,要學會去了解如何從一個網址,渲染出來一個頁面,在到後來可以讓你看到你想看到的東西,並去操作它,瞭解瀏覽器底層的一些渲染機制,是作為一個優秀的前端必不可少的,這篇文章講解的就是有關js單執行緒是如何執行的一篇文章,如有不足之處,請指出,我會及時作出改正。


event loop在我們的日常工作當中,涉及到的地方有很多,只是大家可能不知道這個東西,就是event loop機制導致的這種渲染方式,舉個?:
console.log(1)
setTimeout(()=>{
    console.log(3)
},1)
console.log(2)
複製程式碼

顯而易見,這個結果就是:1、2、3

不管setTimeout放在當前作用域的什麼位置,結果都是1、2、3,為什麼,這就是event loop事件迴圈機制。

大家都知道,js是單執行緒的,如果對多程式和多執行緒不是特別瞭解,可以參考這兩篇文章:

瀏覽器多程式架構

瀏覽器渲染程式多執行緒

通過這兩篇文章你瞭解什麼是多程式多執行緒以後,可能看起來這裡會好很多。

上面的例子,其實還涉及到了一個問題,就是我setTimeout設定的時間是1ms,這裡要注意:

w3c在HTML標準中規定,要求setTimeout時間低於4ms的都按4ms來算

這裡還要注意一點,就是有些時候,為什麼一些大神用js做一些類似於動畫操作的時候,喜歡用setTimeout,而不是用setInterval呢,因為setTimeout是在這個時間後,把當前的方法推到任務佇列裡,而setInterval是強行把當前的方法新增到任務佇列裡,這樣可能會對當前頁面的使用者體驗很不好,可能會出現效果卡頓的情況,所以這裡還要注意一點:

setTimeout是延遲執行,但是不是延遲的時間後立即執行的

其實,event loop它最主要是分三部分:主執行緒、巨集佇列(macrotask)、微佇列(microtask)

js的任務佇列分為同步任務和非同步任務,所有的同步任務都是在主執行緒裡執行的,非同步任務可能會在macrotask或者microtask裡面

主執行緒

主執行緒就是簡單點的理解,就是訪問到的script標籤裡面包含的內容,或者是直接訪問某一個js檔案的時候,裡面的可以在當前作用域直接執行的所有內容(執行的方法,new出來的物件等),都是在主執行緒裡面做的,新增到任務佇列裡面的就不算了。還是老樣子,舉個?:

//index.html
<html>
    <head></head>
    <body>
        <script src="index.js"></script>
        <script>
            console.log(1)
            setTimeout(() => {
               console.log(3) 
            },1)
        </script>
    </body>
</html>

//index.js
console.log(2)
setTimeout(() => {
   console.log(4) 
},1)
複製程式碼

結果1和2都是在主執行緒裡面執行的,3和4被扔到了任務佇列裡面。

巨集佇列(macrotask)

setTimeout、setInterval、setImmediate、I/O、UI rendering

微佇列(microtask)

promise.then、process.nextTick、Object.observe(已廢棄)


先上程式碼
console.log(1)
process.nextTick(() => {
  console.log(8)
  setTimeout(() => {
    console.log(9)
  })
})
setTimeout(() => {
  console.log(2)
  new Promise(() => {
    console.log(11)
  })
})
requestIdleCallback(() => {
  console.log(7)
})
let promise = new Promise((resolve,reject) => {
  setTimeout(() => {
    console.log(10)
  })
  resolve()
  console.log(4)
})
fn()
console.log(3)
promise.then(() => {
  console.log(12)
})
function fn(){
  console.log(6)
}
複製程式碼

結果是1、4、6、3、12、8、2、11、10、9、7

這個寫法可以囊括80%以上的event loop迴圈機制的場景了,下面開始梳理具體的執行機制。

js是從上到下執行的,所以上來先列印的是 1 ,繼續往下走;

遇見了process.nextTick,因為它屬於微佇列(microtask),並且當前主執行緒的程式碼還沒有執行完畢,所以它被展示扔到了微佇列裡,暫時不列印;

這個時候又遇到了setTimeout,setTimeout是屬於巨集佇列(macrotask);

requestIdleCallback,這裡也是不立即執行的,它也不屬於任何佇列,這裡不做詳細解釋;

promise在例項化的時候,這裡的setTimeout繼續被丟到了巨集佇列(macrotask)中,並執行了成功的方法,在有promise.then的呼叫的時候就會去出發,但這裡不做列印,接著發現了console,這裡直接列印 4

fn函式直接呼叫,直接列印 6

console,直接列印 3

promise.then因為它屬於微佇列,但是它在promise例項化的時候被呼叫了,所以它會在微佇列的最前面執行;

到這裡主執行緒裡面就沒有任何可以執行到東西了,下面開始走微佇列(microtask):

由於promise.then被提前呼叫了,所以它會先執行,列印 12

微佇列(microtask)裡面還有一個,就是上面的process.nextTick,執行它,列印 8 ,這個時候發現它有一個setTimeout,放到巨集佇列(macrotask);

到這裡微佇列就走完了,下面開始走巨集佇列(macrotask):

最外面的setTimeout在一開始的時候被放了進去,所以先執行它,列印 2 ,發現它裡面有promise被例項化,直接執行,列印 11

下一個要走的就是promise裡面的setTimeout,列印 10

還剩最後一個setTimeout,就是process.nextTick裡面的,列印 9

到這裡主執行緒、巨集佇列(macrotask)、微佇列(microtask)就全都跑完了,在全部跑完的時候,requestIdleCallback才會執行,列印 7

requesIdleCallback會在當前瀏覽器空閒時期去依次執行,在整個過程當中你可能新增了多個requestIdleCallback,但是都不會執行,只會在空閒時期,去依次根據呼叫的順序就執行。

console.log(1)
process.nextTick(() => {
  console.log(8)
  setTimeout(() => {
    console.log(9)
    requestIdleCallback(() => {
      console.log(13)
    })
  })
})
setTimeout(() => {
  console.log(2)
  new Promise(() => {
    console.log(11)
  })
})
requestIdleCallback(() => {
  console.log(7)
})
let promise = new Promise((resolve,reject) => {
  setTimeout(() => {
    console.log(10)
  })
  resolve()
  console.log(4)
  requestIdleCallback(() => {
    console.log(14)
  })
})
fn()
console.log(3)
promise.then(() => {
  console.log(12)
  requestIdleCallback(() => {
    console.log(15)
  })
})
function fn(){
  console.log(6)
  requestIdleCallback(() => {
    console.log(16)
  })
}
複製程式碼

結果是1、4、6、3、12、8、2、11、10、9、7、14、16、15、13

因為這一塊不涉及到事件迴圈裡面,但是屬於一個比較例外的方法,這裡只做簡單講解,不做深入研究。

總結

其實,event loop用簡單點的話去解釋,就是:

1、先執行主執行緒

2、遇到巨集佇列(macrotask)放到巨集佇列(macrotask)

3、遇到微佇列(microtask)放到微佇列(microtask)

4、主執行緒執行完畢

5、執行微佇列(microtask),微佇列(microtask)執行完畢

6、執行一次巨集佇列(macrotask)中的一個任務,執行完畢

7、執行微佇列(microtask),執行完畢

8、依次迴圈。。。

這個過程,其實就是我們具體要說的js事件迴圈機制(event loop)。


結束語

這些,是我在網上看一些大神的講解,和自己對js事件迴圈機制(event loop)的理解,寫的一篇總結性的文章,如果當中有哪些寫的不對的地方,還請大家指出,我會在最短時間內作出調整。如果覺得謝的還行,幫忙給個贊吧。

相關文章