好程式設計師web前端教程分享JavaScript的執行機制!

好程式設計師IT發表於2019-11-01

  好程式設計師web 前端教程分享JavaScript 的執行機制對於Web 工程師來說了解 JavaScript 的執行機制是很重要的。下面就讓我們一起來看一看吧!

 

1 、關於 JavaScript

 

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

 

2 JavaScript 事件迴圈

 

既然js 是單執行緒,那就像只有一個視窗的銀行,客戶需要排隊一個一個辦理業務,同理 js 任務也要一個一個順序執行。如果一個任務耗時過長,那麼後一個任務也必須等著。那麼問題來了,假如我們想瀏覽新聞,但是新聞包含的超清圖片載入很慢,難道我們的網頁要一直卡著直到圖片完全顯示出來?因此聰明的程式設計師將任務分為兩類:

 

1) 同步任務

2) 非同步任務

 

當我們開啟網站時,網頁的渲染過程就是一大堆同步任務,比如頁面骨架和頁面元素的渲染。而像載入圖片音樂之類佔用資源大耗時久的任務,就是非同步任務。關於這部分有嚴格的文字定義,但本文的目的是用最小的學習成本徹底弄懂執行機制,所以我們用導圖來說明:

 

導圖要表達的內容用文字來表述的話:

 

·     同步和非同步任務分別進入不同的執行 " 場所 " ,同步的進入主執行緒,非同步的進入 Event Table 並註冊函式。

 

·     當指定的事情完成時, Event Table 會將這個函式移入 Event Queue

 

·     主執行緒內的任務執行完畢為空,會去 Event Queue 讀取對應的函式,進入主執行緒執行。

 

·     上述過程會不斷重複,也就是常說的 Event Loop( 事件迴圈 )

 

我們不禁要問了,那怎麼知道主執行緒執行棧為空啊?js 引擎存在 monitoring process 程式,會持續不斷的檢查主執行緒執行棧是否為空,一旦為空,就會去 Event Queue 那裡檢查是否有等待被呼叫的函式。

 

說了這麼多文字,不如直接一段程式碼更直白:

 

1let data = [];

2$.ajax({

3    url:

4    data:data,

5    success:() => {

6        console.log(' 傳送成功 !');

7    }

8})

9console.log(' 程式碼執行結束 ');

上面是一段簡易的ajax 請求程式碼:

 

·      ajax 進入 Event Table ,註冊回撥函式 success

 

·     執行 console.log(' 程式碼執行結束 ')

 

·      ajax 事件完成,回撥函式 success 進入 Event Queue

 

·     主執行緒從 Event Queue 讀取回撥函式 success 並執行。

 

相信透過上面的文字和程式碼,你已經對js 的執行順序有了初步瞭解。接下來我們來研究進階話題: setTimeout

 

3 、關於 setTimeout

 

大名鼎鼎的setTimeout 無需再多言,大家對他的第一印象就是非同步可以延時執行,我們經常這麼實現延時 3 秒執行:

 

1setTimeout(() => {

2    console.log(' 延時 3 ');

3},3000)

漸漸的setTimeout 用的地方多了,問題也出現了,有時候明明寫的延時 3 秒,實際卻 5 6 秒才執行函式,這又咋回事啊?

 

先看一個例子:

 

1setTimeout(() => {

2    task();

3},3000)

4console.log(' 執行 console');

根據前面我們的結論,setTimeout 是非同步的,應該先執行 console.log 這個同步任務,所以我們的結論是:

 

1// 執行 console

2//task()

 

去驗證一下,結果正確!然後我們修改一下前面的程式碼:

 

1setTimeout(() => {

2    task()

3},3000)

4

5sleep(10000000)

乍一看其實差不多嘛,但我們把這段程式碼在chrome 執行一下,卻發現控制檯執行 task() 需要的時間遠遠超過 3 秒,說好的延時三秒,為啥現在需要這麼長時間啊?

這時候我們需要重新理解setTimeout 的定義。我們先說上述程式碼是怎麼執行的:

 

·      task() 進入 Event Table 並註冊 , 計時開始。

 

·     執行 sleep 函式,很慢,非常慢,計時仍在繼續。

 

·      3 秒到了,計時事件 timeout 完成, task() 進入 Event Queue ,但是 sleep 也太慢了吧,還沒執行完,只好等著。

 

·      sleep 終於執行完了, task() 終於從 Event Queue 進入了主執行緒執行。

 

上述的流程走完,我們知道setTimeout 這個函式,是經過指定時間後,把要執行的任務 ( 本例中為 task()) 加入到 Event Queue 中,又因為是單執行緒任務要一個一個執行,如果前面的任務需要的時間太久,那麼只能等著,導致真正的延遲時間遠遠大於 3 秒。

 

我們還經常遇到setTimeout(fn,0) 這樣的程式碼, 0 秒後執行又是什麼意思呢?是不是可以立即執行呢?

 

答案是不會的,setTimeout(fn,0) 的含義是,指定某個任務在主執行緒最早可得的空閒時間執行,意思就是不用再等多少秒了,只要主執行緒執行棧內的同步任務全部執行完成,棧為空就馬上執行。舉例說明:

 

1// 程式碼 1

2console.log(' 先執行這裡 ');

3setTimeout(() => {

4    console.log(' 執行啦 ')

5},0);

 

1// 程式碼 2

2console.log(' 先執行這裡 ');

3setTimeout(() => {

4    console.log(' 執行啦 ')

5},3000);

 

程式碼1 的輸出結果是:

 

1// 先執行這裡

2// 執行啦

3 程式碼 2 的輸出結果是:

4

5// 先執行這裡

6// ... 3s later

7//  執行啦

關於setTimeout 要補充的是,即便主執行緒為空, 0 毫秒實際上也是達不到的。根據 HTML 的標準,最低是 4 毫秒。有興趣的同學可以自行了解。

 

4 、關於 setInterval

 

上面說完了setTimeout ,當然不能錯過它的孿生兄弟 setInterval 。他倆差不多,只不過後者是迴圈的執行。對於執行順序來說, setInterval 會每隔指定的時間將註冊的函式置入 Event Queue ,如果前面的任務耗時太久,那麼同樣需要等待。

 

唯一需要注意的一點是,對於setInterval(fn,ms) 來說,我們已經知道不是每過 ms 秒會執行一次 fn ,而是每過 ms 秒,會有 fn 進入 Event Queue 。一旦 setInterval 的回撥函式 fn 執行時間超過了延遲時間 ms ,那麼就完全看不出來有時間間隔了。

 

5 Promise process.nextTick(callback)

 

傳統的定時器我們已經研究過了,接著我們探究Promise process.nextTick(callback) 的表現。

 

Promise 的定義和功能本文不再贅述,不瞭解的讀者可以學習一下阮一峰老師的 Promise 。而 process.nextTick(callback) 類似 node.js 版的 "setTimeout" ,在事件迴圈的下一次迴圈中呼叫 callback 回撥函式。

 

我們進入正題,除了廣義的同步任務和非同步任務,我們對任務有更精細的定義:

 

·      macro-task( 宏任務 ) :包括整體程式碼 script setTimeout setInterval

 

·      micro-task( 微任務 ) Promise process.nextTick

 

不同型別的任務會進入對應的Event Queue ,比如 setTimeout setInterval 會進入相同的 Event Queue

 

事件迴圈的順序,決定js 程式碼的執行順序。進入整體程式碼 ( 宏任務 ) 後,開始第一次迴圈。接著執行所有的微任務。然後再次從宏任務開始,找到其中一個任務佇列執行完畢,再執行所有的微任務。聽起來有點繞,我們用文章最開始的一段程式碼說明:

 

 1setTimeout(function() {

 2    console.log('setTimeout');

 3})

 4

 5new Promise(function(resolve) {

 6    console.log('promise');

 7}).then(function() {

 8    console.log('then');

 9})

10

11console.log('console');

·     這段程式碼作為宏任務,進入主執行緒。

 

·     先遇到 setTimeout ,那麼將其回撥函式註冊後分發到宏任務 Event Queue ( 註冊過程與上同,下文不再描述 )

 

·     接下來遇到了 Promise new Promise 立即執行, then 函式分發到微任務 Event Queue

 

·     遇到 console.log() ,立即執行。

 

·     好啦,整體程式碼 script 作為第一個宏任務執行結束,看看有哪些微任務?我們發現了 then 在微任務 Event Queue 裡面,執行。

 

·      ok ,第一輪事件迴圈結束了,我們開始第二輪迴圈,當然要從宏任務 Event Queue 開始。我們發現了宏任務 Event

Queue setTimeout 對應的回撥函式,立即執行。 結束。

 

事件迴圈,宏任務,微任務的關係如圖所示:

 

我們來分析一段較複雜的程式碼,看看你是否真的掌握了js 的執行機制:

 

 1console.log('1');

 2

 3setTimeout(function() {

 4    console.log('2');

 5    process.nextTick(function() {

 6        console.log('3');

 7    })

 8    new Promise(function(resolve) {

 9        console.log('4');

10        resolve();

11    }).then(function() {

12        console.log('5')

13    })

14})

15process.nextTick(function() {

16    console.log('6');

17})

18new Promise(function(resolve) {

19    console.log('7');

20    resolve();

21}).then(function() {

22    console.log('8')

23})

24

25setTimeout(function() {

26    console.log('9');

27    process.nextTick(function() {

28        console.log('10');

29    })

30    new Promise(function(resolve) {

31        console.log('11');

32        resolve();

33    }).then(function() {

34        console.log('12')

35    })

36})

第一輪事件迴圈流程分析如下:

 

·     整體 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 。那麼第二輪時間迴圈從 setTimeout1 宏任務開始:

 

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

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

 

宏任務Event Queue

 

微任務Event Queue

 

setTimeout2

 

process2

 

null

 

then2

 

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

 

·     輸出 3

 

·     輸出 5

 

·     第二輪事件迴圈結束,第二輪輸出 2 4 3 5

 

·     第三輪事件迴圈開始,此時只剩 setTimeout2 了,執行。

 

·     直接輸出 9

 

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

 

·     直接執行 new Promise ,輸出 11

 

·     將 then 分發到微任務 Event Queue 中,記為 then3

 

宏任務Event Queue

 

微任務Event Queue

 

null

 

process3

 

null

 

then3

 

·     第三輪事件迴圈宏任務執行結束,執行兩個微任務 process3 then3

 

·     輸出 10

 

·     輸出 12

 

第三輪事件迴圈結束,第三輪輸出9 11 10 12

 

整段程式碼,共進行了三次事件迴圈,完整的輸出為1 7 6 8 2 4 3 5 9 11 10 12 ( 請注意, node 環境下的事件監聽依賴 libuv 與前端環境不完全相同,輸出順序可能會有誤差 )

 

 

 

6 、寫在最後

 

(1)js 的非同步

 

我們從最開頭就說javascript 是一門單執行緒語言,不管是什麼新框架新語法糖實現的所謂非同步,其實都是用同步的方法去模擬的,牢牢把握住單執行緒這點非常重要。

 

(2) 事件迴圈 Event Loop

 

事件迴圈是js 實現非同步的一種方法,也是 js 的執行機制。

 

(3)javascript 的執行和執行

 

執行和執行有很大的區別,javascript 在不同的環境下,比如 node ,瀏覽器, Ringo 等等,執行方式是不同的。而執行大多指 javascript 解析引擎,是統一的。

 

(4)setImmediate

 

微任務和宏任務還有很多種類,比如setImmediate 等等,執行都是有共同點的,有興趣的同學可以自行了解。

 

(5) 最後的最後

 

javascript 是一門單執行緒語言

Event Loop javascript 的執行機制

 

牢牢把握兩個基本點,以認真學習javascript 為中心,早日實現成為前端高手的偉大夢想!


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69913892/viewspace-2662259/,如需轉載,請註明出處,否則將追究法律責任。

相關文章