JavaScript執行機制

以前一直不怎麼理解JS執行機制,直到讀了下面這篇文章

JavaScript執行機制

原文

  簡書原文:https://www.jianshu.com/p/0d2d42fbe1dc

大綱

  1、場景分析
  2、執行機制相關知識點
  3、以實例來說明JavaScript的執行機制
  4、相關概念

1、場景分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

/*

    以下這段代碼的執行結果是什麼?

    如果依照:js是按照語句出現的順序執行這個理念,

    那麼代碼執行的結果應該是:

        //"定時器開始啦"

        //"馬上執行for循環啦"

        //"執行then函數啦"

        //"代碼執行結束"

    但結果並不是這樣的,得到的結果是:

        //"馬上執行for循環啦"

        //"代碼執行結束"

        //"執行then函數啦"

        //"定時器開始啦"

*/

setTimeout(function(){

    console.log('定時器開始啦')

});

 

new Promise(function(resolve){

    console.log('馬上執行for循環啦');

    for(var i = 0; i < 10000; i++){

        i == 99 && resolve();

    }

}).then(function(){

    console.log('執行then函數啦')

});

 

console.log('代碼執行結束');

2、執行機制相關知識點

2.1、關於javascript

  javascript是一門單線程語言,在最新的HTML5中提出了Web-Worker,但javascript是單線程這一核心仍未改變。所以一切javascript版的"多線程"都是用單線程模擬出來的。

2.2、javascript的同步和異步

  單線程就意味着,所有任務需要排隊,前一個任務結束,纔會執行後一個任務。如果前一個任務耗時很長,後一個任務就不得不一直等着。
  如果排隊是因爲計算量大,CPU忙不過來,倒也算了,但是很多時候CPU是閒着的,因爲IO設備(輸入輸出設備)很慢(比如Ajax操作從網絡讀取數據),不得不等着結果出來,再往下執行。
  JavaScript語言的設計者意識到,這時主線程完全可以不管IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回了結果,再回過頭,把掛起的任務繼續執行下去。
  於是,所有任務可以分成兩種,一種是同步任務(synchronous),另一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,只有"任務隊列"通知主線程,某個異步任務可以執行了,該任務纔會進入主線程執行。

 

  1、同步和異步任務分別進入不同的執行"場所",同步的進入主線程,異步的進入Event Table並註冊函數。
  2、當Event Table中指定的事情完成時,會將這個函數移入Event Queue。
  3、主線程內的任務執行完畢爲空,會去Event Queue讀取對應的函數,進入主線程執行。
  4、上述過程會不斷重複,也就是常說的Event Loop(事件循環)。
  5、我們不禁要問了,那怎麼知道主線程執行棧爲空啊?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。

 

 2.3、JavaScript的宏任務與微任務

   你是否覺得同步異步的執行機制流程就是JavaScript執行機制的全部?不是的,JavaScript除了廣義上的的同步任務何異步任務,其對任務還有更精細的定義:
    macro-task(宏任務):包括整體代碼script,setTimeout,setInterval
    micro-task(微任務):Promise,process.nextTick
  不同類型的任務會進入對應的Event Queue。
  事件循環的順序,決定js代碼的執行順序。進入整體代碼(宏任務)後,開始第一次循環。接着執行所有的微任務。然後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行所有的微任務。

 

3、以實例來說明JavaScript的執行機制

 3.1、同步

1

2

3

4

5

6

7

console.log(1);

console.log(2);

console.log(3);

/*

    執行結果:1、2、3

    同步任務,按照順序一步一步執行

*/

3.2、同步和異步

1

2

3

4

5

6

7

8

9

10

console.log(1);

setTimeout(function() {

    console.log(2);

},1000)

console.log(3);

/*

    執行結果:1、3、2

    同步任務,按照順序一步一步執行

    異步任務,放入消息隊列中,等待同步任務執行結束,讀取消息隊列執行

*/

3.3、異步任務進一步分析

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

console.log(1);

setTimeout(function() {

    console.log(2);

},1000)

setTimeout(function() {

    console.log(3);

},0)

console.log(4);

/*

    猜測是:1、4、2、3   但實際上是:1、4、3、2

    分析:

        同步任務,按照順序一步一步執行

        異步任務,當讀取到異步任務的時候,將異步任務放置到Event table(事件表格)

中,當滿足某種條件或者說指定事情完成了(這裏的是時間分別是達到了0ms和1000ms)當指定

事件完成了才從Event table中註冊到Event Queue(事件隊列),當同步事件完成了,便從

Event Queue中讀取事件執行。(因爲3的事情先完成了,所以先從Event table中註冊到

Event Queue中,所以先執行的是3而不是在前面的2)

*/

3.4、宏任務和微任務

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

console.log(1);

setTimeout(function() {

    console.log(2)

},1000);

 

new Promise(function(resolve) {

    console.log(3);

    resolve();

}

).then(function() {

    console.log(4)

});

console.log(5);

/*

    以同步異步的方式來判斷的結果應該是:1、3、5、2、4

    但是事實上結果是:1、3、5、4、2

    爲什麼是這樣呢?因爲以同步異步的方式來解釋執行機制是不準確的,更加準確的方式是宏任務和微任務:

    因此執行機制便爲:執行宏任務 ===> 執行微任務 ===> 執行另一個宏任務 ===> 不斷循環

        即:在一個事件循環中,執行第一個宏任務,宏任務執行結束,執行當前事件循環中的微任務,

執行完畢之後進入下一個事件循環中,或者說執行下一個宏任務

*/

3.5、是否徹底理解JavaScript執行機制實例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

console.log('1');

 

setTimeout(function() {

    console.log('2');

    process.nextTick(function() {

        console.log('3');

    })

    new Promise(function(resolve) {

        console.log('4');

        resolve();

    }).then(function() {

        console.log('5')

    })

})

process.nextTick(function() {

    console.log('6');

})

new Promise(function(resolve) {

    console.log('7');

    resolve();

}).then(function() {

    console.log('8')

})

 

setTimeout(function() {

    console.log('9');

    process.nextTick(function() {

        console.log('10');

    })

    new Promise(function(resolve) {

        console.log('11');

        resolve();

    }).then(function() {

        console.log('12')

    })

})

/*

1、 第一輪事件循環流程分析如下:

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

    遇到setTimeout,其回調函數被分發到宏任務Event Queue中。我們暫且記爲setTimeout1。

    遇到process.nextTick(),其回調函數被分發到微任務Event Queue中。我們記爲process1。

    遇到Promise,new Promise直接執行,輸出7。then被分發到微任務Event Queue中。我們記爲then1。

    又遇到了setTimeout,其回調函數被分發到宏任務Event Queue中,我們記爲setTimeout2。

         

    宏任務Event Queue   微任務Event Queue

    setTimeout1         process1

    setTimeout2         then1

     

    上表是第一輪事件循環宏任務結束時各Event Queue的情況,此時已經輸出了1和7。

    我們發現了process1和then1兩個微任務。

    執行process1,輸出6。

    執行then1,輸出8。

     

    好了,第一輪事件循環正式結束,這一輪的結果是輸出1,7,6,8。

     

2、 那麼第二輪時間循環從setTimeout1宏任務開始:

     

    首先輸出2。接下來遇到了process.nextTick(),同樣將其分發到微任務Event Queue中,

記爲process2。new Promise立即執行輸出4,then也分發到微任務Event Queue中,記爲then2。

     

    宏任務Event Queue     微任務Event Queue

    setTimeout2           process2

                          then2

                           

    第二輪事件循環宏任務結束,我們發現有process2和then2兩個微任務可以執行。

        輸出3。

        輸出5。

        第二輪事件循環結束,第二輪輸出2,4,3,5。

 

3、 第三輪事件循環開始,此時只剩setTimeout2了,執行。

        直接輸出9。

        將process.nextTick()分發到微任務Event Queue中。記爲process3。

        直接執行new Promise,輸出11。

        將then分發到微任務Event Queue中,記爲then3。

         

    宏任務Event Queue     微任務Event Queue

                            process3

                            then3     

    第三輪事件循環宏任務執行結束,執行兩個微任務process3和then3。

        輸出10。

        輸出12。

        第三輪事件循環結束,第三輪輸出9,11,10,12。

 

    整段代碼,共進行了三次事件循環,完整的輸出爲1,7,6,8,2,4,3,5,9,11,10,12。

*/

4、相關概念

4.1、JS爲什麼是單線程的?

   JavaScript語言的一大特點就是單線程,也就是說,同一個時間只能做一件事。那麼,爲什麼JavaScript不能有多個線程呢?這樣能提高效率啊。
  JavaScript的單線程,與它的用途有關。作爲瀏覽器腳本語言,JavaScript的主要用途是與用戶互動,以及操作DOM。這決定了它只能是單線程,否則會帶來很複雜的同步問題。比如,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另一個線程刪除了這個節點,這時瀏覽器應該以哪個線程爲準?
  所以,爲了避免複雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,將來也不會改變。
  爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,允許JavaScript腳本創建多個線程,但是子線程完全受主線程控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單線程的本質。

 4.2、JS爲什麼需要異步?

   如果JS中不存在異步,只能自上而下執行,如果上一行解析時間很長,那麼下面的代碼就會被阻塞。 對於用戶而言,阻塞就意味着"卡死",這樣就導致了很差的用戶體驗。

 4.3、JS單線程又是如何實現異步的呢?

   既然JS是單線程的,只能在一條線程上執行,又是如何實現的異步呢?
  是通過的事件循環(event loop),理解了event loop機制,就理解了JS的執行機制。

 4.4、任務隊列

   "任務隊列"是一個事件的隊列(也可以理解成消息的隊列),IO設備完成一項任務,就在"任務隊列"中添加一個事件,表示相關的異步任務可以進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面有哪些事件。
  "任務隊列"中的事件,除了IO設備的事件以外,還包括一些用戶產生的事件(比如鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列",等待主線程讀取。
  所謂"回調函數"(callback),就是那些會被主線程掛起來的代碼。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數。
  "任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。但是,由於存在後文提到的"定時器"功能,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。
  讀取到一個異步任務,首先是將異步任務放進事件表格(Event table)中,當放進事件表格中的異步任務完成某種事情或者說達成某些條件(如setTimeout事件到了,鼠標點擊了,數據文件獲取到了)之後,纔將這些異步任務推入事件隊列(Event Queue)中,這時候的異步任務纔是執行棧中空閒的時候才能讀取到的異步任務。

 4.5、Event Loop

  主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,所以整個的這種運行機制又稱爲Event Loop(事件循環)。
  Event Loop是javascript的執行機制

 4.6、setTimeout(fn,0)

   setTimeout(fn,0)的含義是,指定某個任務在主線程最早可得的空閒時間執行,也就是說,儘可能早得執行。它在"任務隊列"的尾部添加一個事件,因此要等到同步任務和"任務隊列"現有的事件都處理完,纔會得到執行。
  HTML5標準規定了setTimeout()的第二個參數的最小值(最短間隔),不得低於4毫秒,如果低於這個值,就會自動增加。在此之前,老版本的瀏覽器都將最短間隔設爲10毫秒。另外,對於那些DOM的變動(尤其是涉及頁面重新渲染的部分),通常不會立即執行,而是每16毫秒執行一次。這時使用requestAnimationFrame()的效果要好於setTimeout()。
  需要注意的是,setTimeout()只是將事件插入了"任務隊列",必須等到當前代碼(執行棧)執行完,主線程纔會去執行它指定的回調函數。要是當前代碼耗時很長,有可能要等很久,所以並沒有辦法保證,回調函數一定會在setTimeout()指定的時間執行。

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