搞懂JS執行機制

前言

JS執行機制的問題面試時經常會遇到,以前匆匆忙忙過了幾篇文檔就覺得自己掌握了,然後遇到相關筆試題時,還是懵懵懂懂的,於是下定決心已定要弄懂,於是產生了這篇文章

我們都知道瀏覽器是多線程的,然而JS確是單線程的(可以理解爲瀏覽器只給了一個線程來渲染)

一.關於javascript

大家都知道,javascript是一門單線程語言,雖然有H5的Web-Worker加持,但是創建出來的子線程完全受主線程控制,且不得操作 DOM ,所以還是無法改變JavaScript單線程的本質。所以一切 javascrip t版的“多線程”都是用單線程模擬出來的,一切 javascript 多線程都是紙老虎

1.JS爲什麼是單線程的?

最初設計JS是用來在瀏覽器驗證表單操控DOM元素的是一門腳本語言,如果js是多線程的,那麼兩個線程同時對一個 DOM 元素進行了相互衝突的操作,那麼瀏覽器的解析器是無法執行的。

2.Js爲什麼需要異步?

如果js中不存在異步,只能自上而下執行,如果上一行解析時間很長,那麼下面的代碼就會被阻塞。 對於用戶而言,阻塞就意味着“卡死”,這樣就導致用戶體驗很差。比如在進行ajax請求的時候,如果沒有返回數據後面的代碼就沒辦法執行

3.js單線程又是如何實現異步的呢?

js中的異步以及多線程都可以理解成爲一種“假象”,就拿h5的WebWorker來說,子線程有諸多限制,不能控制DOM,不能修改全局對象等等,通常只用來做計算做數據處理。
這些限制並沒有違揹我們之前的觀點,所以說是“假象”。JS異步的執行機制其實就是事件循環(eventloop),理解了eventloop 機制,就理解了 js 異步的執行機制。

4.JS的事件循環(eventloop)是怎麼運作的?

“事件循環”、“eventloop”、“運行機制” 這三個術語其實說的是同一個東西。
“先執行同步操作異步操作排在事件隊列裏”這樣的理解其實也沒有任何問題,但如果深入的話會引出很多其他概念,比如event table和event queue, 我們來看運行過程:

  1. 首先判斷JS是同步還是異步,同步就進入主線程運行,異步就進入event table.
  2. 異步任務在event table中註冊事件,當滿足觸發條件後,(觸發條件可能是延時也可能是ajax回調),被推入event queue
  3. 同步任務進入主線程後一直執行,直到主線程空閒時,纔會去event queue中查看是否有可執行的異步任務,如果有就推入主線程中。
  4. js解析器會不斷地重複檢查主線程執行棧是否爲空,然後重複第3步,就是Event Loop(事件循環)。

 

 

 

 

 5.那怎麼知道主線程執行棧爲空啊?
js引擎存在monitoring process進程,會持續不斷的檢查 主線程 執行棧是否爲空,一旦爲空,就會去event queue那裏檢查是否有等待被調用的函數。


二.宏任務與微任務:

1.除了廣義的同步任務和異步任務,我們對任務有更精細的定義:

  1. MacroTask(宏觀Task): setTimeout, setInterval, , requestAnimationFrame(請求動畫), I/O

  2. MicroTask(微觀任務): process.nextTick, Promise, Object.observe, MutationObserver

  3. 先同步 再取出第一個宏任務執行 所有的相關微任務總會在下一個宏任務之前全部執行完畢 如果遇見 就 先微後宏

注意: 

宏任務中包含微任務,一定要將宏任務中的微任務執行完,再去執行下一個宏任務

 微任務中有宏任務,則將宏任務放入宏任務隊列任務中

 

2.事件循環機制

不同類型的任務會進入對應的event queue, 比如setTime和setIntval會進入相同(宏任務)的event queue, 而Promise(主要是Promise.then)和process.nextTick會進入相同(微任務)的event queue.

3.s執行順序

所以通常來說,我們頁面中的js執行順序是這樣的:
  • 第一輪事件循環:
  1. 主線程執行js整段代碼(宏任務),將ajax、setTimeout、promise等回調函數註冊到Event Queue,並區分宏任務和微任務。
  2. 主線程提取並執行Event Queue 中的promise,process.nextTick等所有微任務,並註冊微任務中的異步任務到Event Queue。
  • 第二輪事件循環:
  1. 主線程提取Event Queue 中的第一個宏任務(通常是setTimeout)。
  2. 主線程執行setTimeout宏任務,並註冊setTimeout代碼中的異步任務到Event Queue(如果有)。
  3. 執行Event Queue中的所有微任務,並註冊微任務中的異步任務到Event Queue(如果有)。
  • 類似的循環:宏任務每執行完一個,就清空一次事件隊列中的微任務

3.分析

(1)setTimeout

setTimeout(() => {
  console.log('2秒到了')
}, 2000)

setTimeout是異步操作首先進入event table, 註冊的事件就是它的回調,觸發條件就是2秒之後,當滿足條件回調被推入event queue,當主線程空閒時會去event queue裏查看是否有可執行的任務。

 
console.log(1) // 同步任務進入主線程
setTimeout(fun(),0)   // 異步任務,被放入event table, 0秒之後被推入event queue裏
console.log(3) // 同步任務進入主線程

1、3是同步任務馬上會被執行,執行完成之後主線程空閒去event queue(事件隊列)裏查看是否有任務在等待執行,這就是爲什麼setTimeout的延遲事件是0毫秒卻在最後執行的原因

對於我們經常遇到setTimeout(fn,0)這樣的代碼,它的含義是,指定某個任務在主線最早的空閒時間執行,意思就是不用再等多少秒了, 只要主線程執行棧內的同步任務全部執行完成,棧爲空就馬上執行。但是即便主線程爲空,0毫秒實際上也是達不到的。根據HTML的標準,最低是4毫秒。

(2)setIntval

以setIntval(fn,ms)爲例,setIntval是循環執行的,setIntval會每隔指定的時間將註冊的函數置入event queue,不是每過ms會執行一次fn,而是每過ms秒,會有fn進入event queue。需要注意一點的是,一旦setIntval的回調函數fn執行時間超過了延遲事件ms,那麼就完成看不出來有時間間隔了。




(3)Promise與事件循環

Promise在初始化時,傳入的函數是同步執行的,然後註冊then回調。註冊完之後,繼續往下執行同步代碼,在這之前,then的回調不會執行。同步代碼塊執行完畢後,纔會在事件循環中檢測是否有可用的promise回調,如果有,那麼執行,如果沒有,繼續下一個事件循環。

1. 宏任務,微任務都是隊列, 一段代碼執行時,會先執行宏任務中的同步代碼。
2. 進行第一輪事件循環的時候會把全部的js腳本當成一個宏任務來運行。
3. 如果執行中遇到setTimeout之類的宏任務,那麼就把這個setTimeout內部的函數推入[宏任務的隊列]中,下一輪宏任務執行時調用。
4. 如果執行中遇到promise.then()之類的微任務,就會推入到[當前宏任務的微任務隊列]中, 在本輪宏任務的同步代碼都執行完成後,依次執行所有的微任務。
5. 第一輪事件循環中當執行完全部的同步腳步以及微任務隊列中的事件,這一輪事件循環就結束了, 開始第二輪事件循環。
6. 第二輪事件循環同理先執行同步腳本,遇到其他宏任務代碼塊繼續追加到[宏任務的隊列]中,遇到微任務,就會推入到[當前宏任務的微任務隊列]中,在本輪宏任務的同步代碼執行都完成後, 依次執行當前所有的微任務。
7. 開始第三輪循環往復..

下面用代碼來深入理解上面的機制:
 1 setTimeout(function() {
 2     console.log('4')
 3 })
 4 
 5 new Promise(function(resolve) {
 6     console.log('1') // 同步任務
 7     resolve()
 8 }).then(function() {
 9     console.log('3')
10 })
11 
12 console.log('2')

 

  1. 這段代碼作爲宏任務,進入主線程。
  2. 先遇到setTimeout,那麼將其回調函數註冊後分發到宏任務event queue.
  3. 接下來遇到Promise, new Promise立即執行,then函數分發到微任務event queue
  4. 遇到console.log(), 立即執行
  5. 整體代碼script作爲第一個宏任務執行結束, 查看當前有沒有可執行的微任務,執行then的回調。(第一輪事件循環結束了,我們開始第二輪循環)
  6. 從宏任務的event queue開始,我們發現了宏任務event queue中setTimeout對應的回調函數,立即執行。執行結果: 1-2-3-4

 1 console.log('1')
 2 setTimeout(function() {
 3     console.log('2')
 4     process.nextTick(function() {
 5         console.log('3')
 6     })
 7     new Promise(function(resolve) {
 8         console.log('4')
 9         resolve()
10     }).then(function() {
11         console.log('5')
12     })
13 })
14 
15 process.nextTick(function() {
16     console.log('6')
17 })
18 
19 new Promise(function(resolve) {
20     console.log('7')
21     resolve()
22 }).then(function() {
23     console.log('8')
24 })
25 
26 setTimeout(function() {
27     console.log('9')
28     process.nextTick(function() {
29         console.log('10')
30     })
31     new Promise(function(resolve) {
32         console.log('11')
33         resolve()
34     }).then(function() {
35         console.log('12')
36     })
37 })

       1.整體script作爲第一個宏任務進入主線程,遇到console.log(1)輸出1

  1. 遇到setTimeout, 其回調函數被分發到宏任務event queue中。我們暫且記爲setTimeout1
    遇到process.nextTick(),其回調函數被分發到微任務event queue中,我們記爲process1
    遇到Promise, new Promise直接執行,輸出7.then被分發到微任務event queue中,我們記爲then1
  2. 又遇到setTimeout,其回調函數被分發到宏任務event queue中,我們記爲setTimeout2.
  3. 現在開始執行微任務, 我們發現了process1和then1兩個微任務,執行process1,輸出6,執行then1,輸出8, 第一輪事件循環正式結束, 這一輪的結果輸出1,7,6,8.那麼第二輪事件循環從setTimeout1宏任務開始
  4. 首先輸出2, 接下來遇到了process.nextTick(),統一被分發到微任務event queue,記爲process2(注意:宏任務中包含微任務,一定要將宏任務中的微任務執行完,再去執行下一個宏任務  
  5. new Promise立即執行,輸出4,then也被分發到微任務event queue中,記爲then2  
  6. 現在開始執行微任務,我們發現有process2和then2兩個微任務可以執行輸出3,5. 第二輪事件循環結束,第二輪輸出2,4,3,5. 第三輪事件循環從setTimeout2宏任務開始
    10。 直接輸出9,跟第二輪事件循環類似,輸出9,11,10,12
  7. 完整輸出是1,7,6,8,2,4,3,5,9,11,10,12(請注意,node環境下的事件監聽依賴libuv與前端環境不完全相同,輸出順序可能會有誤差)
如果是setTimeout裏面嵌套setTimeout, 那麼嵌套的setTimeout的宏任務要在外面的宏任務排序的後面,往後排。看個例子
 1 new Promise(function (resolve) { 
 2     console.log('1')// 宏任務一
 3     resolve()
 4 }).then(function () {
 5     console.log('3') // 宏任務一的微任務
 6 })
 7 setTimeout(function () { // 宏任務二
 8     console.log('4')
 9     setTimeout(function () { // 宏任務五
10         console.log('7')
11         new Promise(function (resolve) {
12             console.log('8')
13             resolve()
14         }).then(function () {
15             console.log('10')
16             setTimeout(function () {  // 宏任務七
17                 console.log('12')
18             })
19         })
20         console.log('9')
21     })
22 })
23 setTimeout(function () { // 宏任務三
24     console.log('5')
25 })
26 setTimeout(function () {  // 宏任務四
27     console.log('6')
28     setTimeout(function () { // 宏任務六
29         console.log('11')
30     })
31 })
32 console.log('2') // 宏任務一

 

結果:1-2-3-4-5-6-7-8-9-10-11-12
初步總結:宏任務是一個棧按先入先執行的原則,微任務也是一個棧也是先入先執行。但是每個宏任務都對應會有一個微任務棧,宏任務在執行過程中會先執行同步代碼再執行微任務棧。
 

(4)Promise

來看一個案例

1    console.log(1)
2     new Promise(function(resolve,reject){
3         console.log('2')
4         resolve()
5     }).then(function(){
6       console.log(3)
7     })
8     console.log(4)  //1 2 4 3

先看代碼:一個打印,一個new promise,一個promise.then,一個打印

因爲new promise會立即執行,promise.then是異步操作且是微任務

所以,先執行第一個打印,執行new Promise,將promise.then放入微任務隊列,接着執行第二個打印,再執行微任務隊列中的promise.then

最後結果是:1 2 4 3

(5)async/await

async/await是什麼

我們創建了promise但不能同步等待它執行完成。 我們只能通過then傳咦個回調函數這樣很容易再次陷入promise的回調地獄。 實際上, async/await在底層轉換成了promise和then回調函數,也就是說, 這是promise的語法糖。每次我們使用await, 解釋器都創建咦個promise對象,然後把剩下的async函數中的操作放到then回調函數中。 async/await的實現,離不開promise. 從字面意思來理解, async是“異步”的簡寫,而await是async wait的簡寫可以認爲是等待異步方法執行完成。

async/await用來幹什麼

用來優化promise的回調問題,被稱爲是異步的終極解決方案

async/await內部做了什麼

async函數會返回一個Promise對象,如果在函數中return一個直接量(普通變量),async會把這個直接量通過Promise.resolve()封裝成Promise對象。如果你返回了promise那就以你返回的promise爲準。await是在等待,等待運行的結果也就是返回值。await後面通常是一個異步操作(promise),但是這不代表await後面只能跟異步才做,await後面實際是可以接普通函數調用或者直接量。
async相當於 new Promise,await相當於then

await的等待機制

如果await後面跟的不是一個promise,那await後面表達式的運算結果就是它等到的東西,如果await後面跟的是一個promise對象,await它會'阻塞'後面的diamante,等着promise對象resolve, 然後得到resolve的值作爲await表達式的運算結果。但是此"阻塞"非彼“阻塞”,這就是await必須用在async函數中的原因。 async函數調用不會造成"阻塞",它內部所有的“阻塞”都被封裝在一個promise對象中異步執行(這裏的阻塞理解成異步等待更合理)

async/await在使用過程中有什麼規定

每個async方法都返回一個promise, await只能出現在async函數中

async/await在什麼場景使用

單一的promise鏈並不能發現async/await的有事,但是如果需要處理由多個promise組成的then鏈的時候,優勢就能體現出來了(Promise通過then鏈來解決多層回調的問題,現在又用async/awai來進一步優化它)

async/await的使用方法暫時不說了,它的寫法更優雅一些,要比promise的鏈接調用更多直觀也易於維護

我們來看在任務隊列中async/await的運行機制,先給出大概方向再通過案例來證明:

  1. async定義的是一個promise函數和普通函數一樣只要不調用就不會進入事件隊列。
  2. async內部如果沒有主動return promise, 那麼async會把函數的返回值用promise包裝
  3. await關鍵字必須出現在async函數中,await後面不是必須要跟一個異步操作,也可以是一個普通表達式
  4. 遇到await關鍵字,await右邊的語句會被立即執行然後await下面的代碼進入等待狀態,等待await得到結果。
  5. await後面如果不是promise對象,await會阻塞後面的代碼,先執行async外面的同步代碼,同步代碼執行完,再回到async內部,把這個非promise的東西,作爲await表達式的結果。
  6. await後面如果是promise對象,await也會暫停async後面的代碼,先執行async外面的同步代碼,等着promise對象fulfilled,然後把resolve的參數作爲await表達式的運算結果

看一下下面的案例:

 

 

 

 從這兩個案例可以得出:async/await在執行時,如果有return ,則會先跳出sync/await函數執行外部的主線程程序,最後才執行async/await函數的return

 

(6)process.nextTick

JS執行時會直接作爲微任務放入Task Queque中,當主線程的任務執行完畢後,按微任務queue順序執行

 

三.實踐

1.先來一劑猛藥,某面試題

 1 console.log('1');
 2 setTimeout(() => {
 3     console.log('9');
 4     this.$nextTick(() =>  {
 5         console.log('11');
 6     });
 7     new Promise(function(resolve) {
 8         console.log('10');
 9         resolve();
10     }).then(function() {
11         console.log('12')
12     });
13 },5000);
14 this.$nextTick(() =>  {
15     console.log('3');
16 });
17 new Promise(function(resolve) {
18     console.log('2');
19     resolve();
20 }).then(function() {
21     console.log('4');
22 });
23 setTimeout(() => {
24     console.log('5');
25     this.$nextTick(() =>  {
26         console.log('7');
27     });
28     new Promise(function(resolve) {
29         console.log('6');
30         resolve();
31     }).then(function() {
32         console.log('8');
33     });
34 });

先看答案:1-2-3-4-5-6-7-8-9-10-11-12

分析:
看到諸多異步延時任務先不要慌,一步一步來解讀,代碼中的this.$nextTick(callback)千萬不要解讀成上面的process.nextTick(callback),否則你會被坑慘的,process是nodeJs裏面的,nodeJs執行機制和JavaScript的執行機制是不同的,nodeJs不會看你代碼的層級關係哦,只關心你的事件的類型,按照這個順序來執行代碼,而我們的js是按照父級的事件,有着層級關係的執行。
vueJs的主線程先執行,首先打印出1,第一個setTimeout push到macro task,nextTick放入micro task,Promise立即執行,then push進micro task,第二個setTimeout push到macro task,接着執行micro task,打印3 4,最後執行macro task,注意這裏有個坑,macro task裏面有兩個timer,第一個5000ms之後執行,所以先執行第二個,所以最後的答案小學生都知道,打印順序從1到12。

 2.

 

3.

 1 console.log('1');
 2 // 記作 set1
 3 setTimeout(function () {
 4     console.log('2');
 5     // set4
 6     setTimeout(function() {
 7         console.log('3');
 8     });
 9     // pro2
10     new Promise(function (resolve) {
11         console.log('4');
12         resolve();
13     }).then(function () {
14         console.log('5')
15     })
16 })
17 
18 // 記作 pro1
19 new Promise(function (resolve) {
20     console.log('6');
21     resolve();
22 }).then(function () {
23     console.log('7');
24     // set3
25     setTimeout(function() {
26         console.log('8');
27     });
28 })
29 
30 // 記作 set2
31 setTimeout(function () {
32     console.log('9');
33     // 記作 pro3
34     new Promise(function (resolve) {
35         console.log('10');
36         resolve();
37     }).then(function () {
38         console.log('11');
39     })
40 })

分析:

1.整體script作爲第一個宏任務進入主線程,遇到console.log,輸出1。

2.遇到第一個setTimeout-set1,其回調函數被分發到宏任務Event Queue中

3.遇到new Promise 直接打印,輸出6,Promise.then-pro1被分發到微任務Event Queue中

4.遇到第二個setTimeout-set2,其回調函數被分發到宏任務Event Queue中

5. 主線程的整段js代碼(宏任務)執行完,開始清空所有微任務:主線程執行微任務pro1,輸出7;遇到第三個setTimeout-set3,註冊回調函數分發到宏任務Event Queue中。

  第一輪EvenLoop結束,開始第二輪

6.主線程執行隊列中第一個宏任務set1,輸出2;代碼中遇到了第四個setTimeout-set4,註冊回調;又遇到了pro2,new promise()直接執行輸出4,並註冊回調;

7. set1宏任務執行完畢,開始清空微任務,主線程執行微任務pro2,輸出5。(宏任務中包含微任務,一定要將宏任務中的微任務執行完,再去執行下一個宏任務

第二輪EvenLoop結束,開始第三輪

8.主線程執行隊列中第一個宏任務set2,輸出9;代碼中遇到了pro3,new promise()直接輸出10,並註冊回調;

9.set2宏任務執行完畢,開始情況微任務,主線程執行微任務pro3,輸出11。

 ...

以此類似循環,最後輸出結果爲:1、6、7、2、4、5、9、10、11、8、3

 

 

 

就講到這兒吧,你會了嗎?

再來一個題檢測一下吧:

 1 console.log('1');
 2     
 3     setTimeout(function () {
 4       console.log('2');
 5       new Promise(function (resolve) {
 6         console.log('3');
 7         resolve();
 8       }).then(function () {
 9         console.log('4')
10       })
11     },0)
12   
13     new Promise(function (resolve) {
14       console.log('5');
15       resolve();
16     }).then(function () {
17       console.log('6')
18     })
19 
20     setTimeout(function () {
21       console.log('7');
22       new Promise(function (resolve) {
23         console.log('8');
24         resolve();
25       }).then(function () {
26         console.log('9')
27       })
28       console.log('10')
29     },0)
30    
31     console.log('11') 

答案: 1  5 11 6 2 3  4 7 8  10 9

 

參考:https://juejin.im/post/6844903667301089288

          https://www.cnblogs.com/yaya-003/p/12875191.html

         https://www.jianshu.com/p/1368d375aa66

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章