Java程式設計師從笨鳥到菜鳥(五十一) 徹底弄懂 JavaScript 的執行機制

明割啦發表於2018-08-15

前言

JavaScript 是一門單執行緒語言,這樣就可以得出結論:JavaScript 是按照語句出現順序執行的
正是因為 JavaScript 是一行一行執行的,所以以為 js 是這樣的:

var a = '1';
console.log(a);
var b = '2';
console.log(b);

但是實際上 js 是這樣的:

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('程式碼執行結束');

但是依照* js 按照語句出現順序執行*的理念,瀏覽器控制檯輸出的結果如下:

定時器開始啦
馬上執行 for 迴圈啦
執行 then 函式啦
程式碼執行結束

這結果當然是不正確的,要不文章題目也不叫徹底弄懂執行機制,接下來就說到關於 JavaScript 的執行機制

關於 JavaScript

javascript 是一門單執行緒語言,一切 JavaScript 版的多執行緒都是單執行緒模擬出來的

JavaScript 時間迴圈

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

  • 同步任務
  • 非同步任務
    當我們開啟網頁的時候,網頁的渲染過程就是一大堆同步任務,比如頁面元素和骨架的渲染;而像載入圖片、音樂之類的檔案耗時過久的任務,就是非同步任務。先以圖解的方式來演示 JavaScript 的執行機制:
    這裡寫圖片描述

分析:

  • 同步任務和非同步任務分別進入不同的場所,同步的進入主執行緒,非同步的進入 Event Table 並註冊函式
  • 當指定的事情完成時,Event Table 會將這個函式移入 Event Queue
  • 主執行緒的任務執行完畢為空,會去 Event Queue 讀取對應的函式,進入主執行緒執行
  • 以上過程會不斷重複,也就是通常所說的 Event Loop (事件迴圈 javascript 的執行機制)
    如何判斷主執行緒執行為空? js 引擎存在 monitoring process 程式,會持續不斷地檢查主執行緒執行棧是否為空,一旦為空,就會去 Event Queue 那裡檢查是否有等待被呼叫的函式。

下面使用一段 ajax 程式碼來說明:

var data = [];
$.ajax({
    url:www.javascript.com,
    data:data,
    success:() => {
        console.log('傳送成功!');
    }
})
console.log('程式碼執行結束');

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

  • ajax 進入 Event Table,註冊回撥函式 success
  • 執行 console.log(‘程式碼執行結束’)
  • ajax 時間完成,回撥函式 success 進入 Event Queue
  • 主執行緒從 Event Queue 讀取回撥函式 success 執行

setTimeout

setTimeout 就是非同步可以延遲執行,經常可以這樣,延時 3 秒執行

setTimeout(function() {
    console.log('執行任務');
    task();
}, 3000);

但是正常在使用過程中,就會出現問題,明明是延遲 3 秒,實際卻是 5、6 秒才執行函式,先看一個例子:

setTimeout(function task() {
    console.log('開始執行任務');
    task();
},3000);
console.log(‘執行 console’);

根據得出的結論,setTimeout 是非同步的,應該限制性 console.log 這個同步任務,控制檯列印的語句應該是:

執行 console
開始執行任務

修改一下程式碼:

setTimeout(function task() {
    console.log('開始執行任務');
    task();
},3000);
sleep(100000);

會發現 console 列印語句的時間遠遠大於 3 秒,先看下程式是怎樣執行的

  • 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) 的含義是,指定某個任務在主執行緒最早可得的空閒時間執行,意思就是不用再等多少秒了,只要主執行緒執行棧內的同步任務全部執行完成,棧為空就馬上執行。
關於setTimeout要補充的是,即便主執行緒為空,0 毫秒實際上也是達不到的。根據HTML的標準,最少要 4 ms

setInterval

setInterval 是迴圈執行,對於執行順序來說,setInterval 會每隔指定的時間將祖冊的函式置入 Event Queue,如果耗時太久,一樣需要等待。

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

Promise 與 process.nextTick(callback)

Promise:是非同步程式設計的一種解決方案,簡單說就是一個容器,裡面儲存著某個非同步操作的結果,從語法上來說,promise 是一個物件,他可以獲取非同步操作的訊息,有三種狀態:

  • pending:進行中
  • fullfield:已經成功
  • rejected:已經失敗
    狀態改變:
  • 從 pending 變為 fullfield
  • 從 pending 變為 rejected

  • process.nextTick(callback) 類似 node.js 版的 “setTimeout”,在事件迴圈的下一次迴圈中呼叫 callback 回撥函式

除了廣義的同步任務和非同步任務,還有巨集任務和微任務

  • macro-task(巨集任務):包括整體程式碼 script, setTimeout, setInterval
  • mocro-task(微任務):Promise, process.nextTrick
    不同型別的任務會進入對應的 Event Queue

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

setTimeout(function() {
    console.log('setTimeout');
})

new Promise(function(resolve) {
    console.log('promise');
}).then(function() {
    console.log('then');
})

console.log('console');

輸出結果:

promise
console
then
setTimeout

執行順序:

  • 這段程式碼作為巨集任務,進入主執行緒
  • 先遇到 setTimeout, 那麼將其回撥函式註冊分發到巨集任務 Event Queue
  • 接下來遇到 Promise, new Promise 立即執行, then 函式分配到微任務 Event Queue
  • 遇到 console, 立即執行
  • 整段程式碼作為第一個巨集任務執行結束,之後在檢視有哪些微任務,發現了 then 在微任務中,立即執行
  • 第一輪事件迴圈結束了,開始第二輪迴圈,先從巨集任務 Event Queue 開始,發現了 setTimeout 對應的回撥函式,立即執行
  • 結束

時間迴圈、巨集任務、微任務的關係如下圖:
這裡寫圖片描述

接下來看一段稍微複雜的程式碼:

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
7
6
8
2
4
3
5
9
11
10
12

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

  • 整體 Script 作為第一個巨集任務進入主執行緒,遇到 console,輸出 1
  • 遇到 setTimeout, 其回撥函式被分發到巨集任務 Event Queue 中,暫且記為 setTimeout1
  • 遇到 process.nextTick(),其回撥函式被分發到微任務 Event Queue 中,記為 proccess1
  • 遇到 promise, 直接執行,輸出 7, then 被分發到微任務 Event Queue, 記為 then1
  • 又遇到了 setTimeout, 其回撥函式被分發到巨集任務 Event Queue 中,記為 setTimeout2
巨集任務 Event Queue 微任務 Event Queue
setTimeout1 process1

setTimeout2|then1|

  • 第一輪事件迴圈巨集任務結束,發現了 proccess1 和 then1 兩個微任務
  • 執行 process1,輸出 6
  • 執行 then1,輸出 8
    第一輪事件迴圈正式結束,這一輪結果輸出是 1,7,6,8,那麼第二輪事件迴圈從 setTimeout1 巨集任務開始
  • 首先遇到 console,輸出 2,接下來遇到 proccess.nextTick(),同樣將其分發到微任務Event Queue中,記為process2。new Promise立即執行輸出4,then也分發到微任務Event Queue中,記為then2
巨集任務 Event Queue 微任務 Event Queue
setTimeout2 process2
then2

- 第二輪事件迴圈巨集任務結束,發現有 proccess2 和 then2 兩個為任務執行
- 執行 proccess2,輸出 3
- 執行 then2,輸出 5
第二輪事件迴圈正式結束,這一輪的輸出結果是 2,4,3,5,那麼第三輪事件迴圈從 setTimeout2 巨集任務執行
- 遇到 console 直接輸出 9,
- 將 process.nextTick() 分發到微任務 Event Queue 中。記為 process3
- 直接執行 new Promise,輸出11
- 將 then 分發到微任務 Event Queue 中,記為 then3

巨集任務 Event Queue 微任務 Event Queue
proccess3
then3

- 第三輪事件迴圈巨集任務執行結束,執行兩個微任務 proccess3 和 then3
- 輸出 10,
- 輸出 12
- 第三輪事件迴圈結束,第三輪輸出的是 9,11,20,12
整段程式碼,進行了三次事件迴圈,完整輸出為: 1,7,6,8,2,4,3,5,9,11,10,12

原文傳送門:https://juejin.im/post/59e85eebf265da430d571f89 非常感謝作者

相關文章