瀏覽器內的線程
眾所周知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---')
在我電腦上輸出的是:
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的等待時間結束后並不是直接執行的而是先推入瀏覽器的一個任務隊列,在同步隊列結束后在依次調用任務隊列中的任務。
這就牽涉到瀏覽器的幾個線程了,一般來說瀏覽器會有以下幾個線程
-
js引擎線程 (解釋執行js代碼、用戶輸入、網絡請求)
-
GUI線程 (繪制用戶界面、與js主線程是互斥的)
-
http網絡請求線程 (處理用戶的get、post等請求,等返回結果后將回調函數推入任務隊列)
-
定時觸發器線程 (setTimeout、setInterval等待時間結束后把執行函數推入任務隊列中)
-
瀏覽器事件處理線程 (將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,如此往復循環至結束
如果你喜歡我們的文章,關注我們的公眾號和我們互動吧。