瀏覽器內的事件隊列

瀏覽器內的線程

衆所周知JavaScript是基於單線程運行的,同時又是能夠異步執行的,通常來講這種既是單線程又是異步的語言都是基於事件來驅動的,剛好瀏覽器就給JavaScript提供了這麼一個環境javascript

 1 setTimeout(function(argument) {
 2   console.log('---1---')
 3 }, 0)
 4 
 5 console.time("test")
 6 for (var i = 0; i < 1000000; i++) {
 7   i === (100000 - 1)
 8 }
 9 console.timeEnd("test")
10 
11 console.log('---2---')

 

在我電腦上輸出的是:html

1 test: 5.4892578125ms
2 
3 ---2---
4 
5 ---1---

 

咦,它不講道理啊,明明我設置的是0毫秒以後打印‘---1---’的
有狀況,打開前端聖經瞧瞧,裏頭有句話:
This is because even though setTimeout was called with a delay of zero, it's placed on a queue and scheduled to run at the next opportunity; not immediately. Currently-executing code must complete before functions on the queue are executed, thus the resulting execution order may not be as expected.
原來setTimeout的等待時間結束後並非直接執行的而是先推入瀏覽器的一個任務隊列,在同步隊列結束後在依次調用任務隊列中的任務。前端

這就牽涉到瀏覽器的幾個線程了,通常來講瀏覽器會有如下幾個線程java

  1. js引擎線程 (解釋執行js代碼、用戶輸入、網絡請求)git

  2. GUI線程 (繪製用戶界面、與js主線程是互斥的)web

  3. http網絡請求線程 (處理用戶的get、post等請求,等返回結果後將回調函數推入任務隊列)api

  4. 定時觸發器線程 (setTimeout、setInterval等待時間結束後把執行函數推入任務隊列中)promise

  5. 瀏覽器事件處理線程 (將click、mouse等交互事件發生後將這些事件放入事件隊列中)瀏覽器

JavaScript函數的執行棧及回調

 1 function test1() {
 2   test2()
 3   console.log('你們好,我是test1')
 4 }
 5 
 6 function test2() {
 7   console.log('你們好,我是test2')
 8 }
 9 
10 function main() {
11   console.log('你們好,我是main')
12   setTimeout(() => {
13     console.log('你們好,我是setTimeout')
14   }, 0)
15   test1()
16 }
17 
18 main()

 

執行結果以下:網絡

你們好,我是main

你們好,我是test2

你們好,我是test1

你們好,我是setTimeout

當咱們調用一個函數,它的地址、參數、局部變量都會壓入到一個 stack 中

step1:main()函數首先被調用,進入執行棧打印‘你們好,我是main’

step2:遇到setTimeout,將回調函數放入任務隊列中

step3:main調用test1,test1函數進入stack中將被執行

step4:test1執行,test1調用test2

step5:test2執行,打印‘你們好,我是test2’

step6:test2執行完畢從stack彈出回到test1,打印‘你們好,我是test1’

step6:主線程執行完畢,進入callback queue隊列執行setTimeout的回調函數打印‘你們好,我是setTimeout’ 至此整個程序執行完畢,不過event loop一直在等待着其餘的回調函數。

用代碼表示的話大概是這樣

1 while (queue.waitForMessage()) {
2   queue.processNextMessage()
3 }

 

這個線程會一直在等待其餘回調函數過來例如click、setTimeout等

用一張圖來表示以下所示:

macrotask 與 microtask

接下來看段代碼想一想這個的運行結果是怎樣的

 1 setTimeout1 = setTimeout(function() {
 2     console.log('---1---')
 3   }, 0)
 4 
 5   setTimeout2 = setTimeout(function() {
 6     Promise.resolve()
 7       .then(() => {
 8         console.log('---2---')
 9       })
10     console.log('---3---')
11   }, 0)
12 
13   new Promise(function(resolve) {
14 
15     console.time("Promise")
16     for (var i = 0; i < 1000000; i++) {
17       i === (1000000 - 1) && resolve()
18     }
19     console.timeEnd("Promise")
20 
21   }).then(function() {
22     console.log('---4---')
23   });
24 
25   console.log('---5---')

 

在上面咱們有分析到瀏覽器會將異步的回調函數放進一個任務隊列中去,按照此思路分析當程序運行的時候首先會遇到setTimeout函數,並將setTimeout的回調函數放入任務隊列中去,繼續往下執行又會遇到個setTimeout函數,不過這個setTimeout的回調函數有些例外,回調函數中又有個Promise對象,不過這個咱暫且無論等到任務隊列裏頭輪到它執行再說,接下來瀏覽器的解釋器會遇到個正在new Promise對象的操做,這個總算不是異步執行的了,首先會開始一個程序運行計時器,隨後會輸出從0自增到1000000所用的時間,在i = 999999時,Promise的狀態會變成resolve隨後將resolve所要執行的回調函數推入任務隊列,最後執行到程序末尾,此時輸出的是‘---5---`。

按照上面的分析咱們的程序輸出順序是:

程序從0自增到1000000所須要的時間

---5---

---1---

---3---

---4---

---2---

用一張圖來表示以下所示:

接下來給扔瀏覽器裏頭執行一遍看下結果,結果以下:

Promise: 5.151123046875ms

---5---

---4---

---1---

---3---

---2---

why

由於瀏覽器的任務隊列不止一個,還有 microtasks 和 macrotasks

microtasks:

  • process.nextTick
  • promise
  • Object.observe
  • MutationObserver

macrotasks:

  • setTimeout:
  • setInterval
  • setImmediate
  • I/O
  • UI渲染

據whatwg規範介紹

  • 一個事件循環(event loop)會有一個或多個任務隊列(task queue)
  • 每個 event loop 都有一個 microtask queue
  • task queue == macrotask queue != microtask queue
  • 一個任務 task 能夠放入 macrotask queue 也能夠放入 microtask queue 中
  • 調用棧清空(只剩全局),而後執行全部的microtask。當全部可執行的microtask執行完畢以後。循環再次從macrotask開始,找到其中一個任務隊列執行完畢,而後再執行全部的microtask,這樣一直循環下去

so,正確的執行步驟應該爲:

step1:執行腳本,把這段腳本壓入task queue,此時

1   stacks: []
2   task queue: [script]
3   microtask queue: []

 

step2:遇到setTimeout1,setTimeout可看成一個task,因此此時

1  stacks: [script]
2   task queue: [setTimeout1]
3   microtask queue: []

 

step3:繼續往下執行,遇到setTimeout2,將setTimeout2壓入task queue中

1   stacks: [script]
2   task queue: [setTimeout1,setTimeout2]
3   microtask queue: []

 

step4:繼續往下執行,new Promise,按同步流程往下走,輸出new Promise內的執行時間

1   stacks: [script]
2   task queue: [setTimeout1,setTimeout2]
3   microtask queue: []

 

step5:在i = 99999的時候,resolve()發生,把回調成功的代碼段壓入microtask queue,此時

1   stacks: [script]
2   task queue: [setTimeout1,setTimeout2]
3   microtask queue: [console.log('---4---')]

 

step6:繼續往下執行就是console.log('---5---'),此時stacks隊列裏頭的任務結束

1   stacks: []
2   task queue: [setTimeout1,setTimeout2]
3   microtask queue: [console.log('---4---')]

 

step7:stacks隊列爲空,此時事件輪詢線程會將microtask queue隊列裏頭的任務推入stacks中去,而後執行輸出’---4---‘

1   stacks: []
2   task queue: [setTimeout1,setTimeout2]
3   microtask queue: []

 

step8:stacks隊列又爲空了且microtask queue隊列也是空着的,這時就會開始執行task queue裏頭的任務了,首先是setTimeout1,輸出’---1---‘

1   stacks: [setTimeout1,setTimeout2]
2   task queue: []
3   microtask queue: []

 

step8:接着setTimeout2在裏頭遇到個Promise.resolve(),將回調成功的函數壓入microtask queue隊列裏頭,而後輸出’---3---‘

1   stacks: [setTimeout2]
2   task queue: []
3   microtask queue: [Promise.resolve()]

 

step9:當setTimeout2執行完畢以後執行microtask queue,輸出’---2---‘,至此該代碼段執行完畢

1   stacks: []
2   task queue: []
3   microtask queue: []

 

用一句話簡單的來講:整個的js代碼macrotask先執行,同步代碼執行完後有microtask執行microtask,沒有microtask執行下一個macrotask,如此往復循環至結束

 

若是你喜歡咱們的文章,關注咱們的公衆號和咱們互動吧。

相關文章
相關標籤/搜索