Java程式設計師從笨鳥到菜鳥(五十一) 徹底弄懂 JavaScript 的執行機制
前言
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 非常感謝作者
相關文章
- Java程式設計師從笨鳥到菜鳥全部部落格目錄Java程式設計師
- Java程式設計師從笨鳥到菜鳥(五十二) 配置檔案實現將返回 POJO 類直接轉換成 json 物件Java程式設計師POJOJSON物件
- 深入理解JavaScript之徹底弄懂JsEventLoop執行機制JavaScriptJSOOP
- 程式設計師菜鳥面試攻略程式設計師面試
- Java集合從菜鳥到大神演變Java
- 給程式設計師“菜鳥”的6條建議程式設計師
- java從菜鳥到碼神之路——運算子Java
- 程式設計師職業發展路徑圖:從菜鳥工程師到高階架構師程式設計師工程師架構
- Linux“菜鳥”到“菜鳥的一些建議Linux
- 這一次,徹底弄懂JS執行機制(Event Loop)JSOOP
- Python從菜鳥到高手:分片(Slicing)Python
- 徹底弄懂Javascript中的thisJavaScript
- java菜鳥入門Java
- 菜鳥的架構師之路架構
- 程式設計25年後,現實將我打回菜鳥程式設計師的起點程式設計師
- 從CPU Cache出發徹底弄懂volatile/synchronized/cas機制synchronized
- 做了三年還覺得自己是菜鳥程式設計師程式設計師
- 好程式設計師web前端教程分享JavaScript的執行機制!程式設計師Web前端JavaScript
- 好程式設計師web前端分享菜鳥Vue學習筆記(二)程式設計師Web前端Vue筆記
- 菜鳥求助!!!
- Linux菜鳥到老鳥的那些建議Linux
- 給程式設計菜鳥的16條忠告,你做到幾條程式設計
- 轉:徹底弄懂HTTP快取機制及原理HTTP快取
- 菜鳥程式設計師都是怎樣寫程式碼的?你也可以學一手程式設計師
- 從 synchronized 到 CAS 和 AQS - 徹底弄懂 Java 各種併發鎖synchronizedAQSJava
- 從 synchronized 到 CAS 和 AQS – 徹底弄懂 Java 各種併發鎖synchronizedAQSJava
- 菜鳥市場
- 菜鳥筆記之計算機網路(4)筆記計算機網路
- 精益生產從菜鳥到高手,你需要的是這些!
- 從只會git add .的菜鳥到掌握git基本功能Git
- Golang快速入門:從菜鳥變大佬Golang
- JavaScript 非同步及Promise 菜鳥學習心得JavaScript非同步Promise
- 從六西格瑪菜鳥到高手,這些你都需要!
- hashmap == 菜鳥驛站?HashMap
- 菜鳥看前端(Git)前端Git
- JavaScript的程式碼執行機制JavaScript
- 菜鳥裹裹App分析系列-UI框架設計分析APPUI框架
- 寶鯤財經:投資交易從菜鳥到大神的必經之路