JavaScript執行機制的理解
0.前言
又開始新一輪的跳槽季,金三銀四,希望大家找到好的工作。

timg.jpeg
1.簡介
JavaScript 是指令碼語言,JS也是單執行緒的,因此同一時間只能做一件事,也就是說它一次僅能處理一個任務。
思考題
event loop()
console.log(1) Promise.resolve().then(function () { console.log(2) }) new Promise(function(resolve, reject){ console.log(3) resolve() }).then(function () { console.log(4) setTimeout(function () { console.log(5) }) }) console.log(6) setTimeout(function () { Promise.resolve().then(function () { console.log(7) setTimeout(function () { console.log(8) }) }) })
2.概念
執行上下文(Execution Context)
執行上下文簡單來說就是一個執行環境。它有全域性環境、函式環境和 eval
函式環境之分。它會在javascript引擎執行你的指令碼的時候去建立。
執行棧(Execution Stack)
執行棧也就是常說的呼叫棧,它是一種擁有LIFO(後進先出)的資料結構。它會儲存程式碼執行時建立的執行上下文
微任務(micro task)與巨集任務(macro task)
javasript中的任務分為微任務和巨集任務兩種,這兩種任務的執行時機是不同的,因此區分js中哪些是巨集任務,哪些是微任務則十分重要。我們常見的巨集任務有: script
任務、 setTimeout
、 ajax
等,常見的微任務比較典型的是: Promise.resolve().then()
、 process.nextTick
、 MutationObserver
等。
事件迴圈(event loop)
javasript是單執行緒的,一次僅能處理一個任務。但js所在的宿主環境,也就是我們所說的瀏覽器並不是單執行緒的(這裡宿主環境僅討論瀏覽器)。它在遇到一些任務時,比如說 setTimeout
、 event listener
等。它會告訴瀏覽器:老兄幫個忙,事成後通知我一聲,小弟我先幹別的事去了。瀏覽器會迴應說:交給我吧,小老弟,事成後我放到任務佇列,自己去取啊。於是,javasript開始執行 script
任務,執行完了就開始檢查有沒有微任務啊,沒有的話就從任務佇列開始取巨集任務執行,每執行完一次巨集任務,就去看看有沒有微任務,有的話就執行完成,再執行巨集任務,如此往復。如下圖:

1.png
而準確的劃分方式是:
-
macro-task
(巨集任務):包括整體程式碼script
,setTimeout
,setInterval
-
micro-task
(微任務):Promise
,process.nextTick
3.回答思考題
JavaScript為什麼是單執行緒的?
因為現在如果有兩個任務一個是刪除DOM節點,一個是增加DOM節點,瀏覽器該如何執行?所以JavaScript是單執行緒
為什麼需要非同步?
如果JavaScript中不存在非同步,只能自上而下執行,如果上一行解析時間很長,那麼下面的程式碼就會被阻塞,不向下執行。
頁面出來,使用者看到覺得是“卡死了”,所以需要非同步。
JavaScript單執行緒又是如何實現非同步的呢?
是通過的事件迴圈(event loop)實現非同步的。
JavaScript中的event loop()
JavaScript的執行機制是
- 首先判斷JavaScript是同步還是非同步,同步就進入主執行緒,非同步就進入
event table
- 非同步任務在
event table
中註冊函式,當滿足觸發條件後,被推入event queue
- 同步任務進入主執行緒後一直執行,直到主執行緒空閒時,才會去
event queue
中檢視是否有可執行的非同步任務,如果有就推入主執行緒中
瞭解了這幾個概念,再來看看javascript是怎麼執行程式碼的就比較輕鬆愉快了。開始吧
console.log(1) Promise.resolve().then(function () { console.log(2) }) new Promise(function(resolve, reject){ console.log(3) resolve() }).then(function () { console.log(4) setTimeout(function () { console.log(5) }) }) console.log(6) setTimeout(function () { Promise.resolve().then(function () { console.log(7) setTimeout(function () { console.log(8) }) }) })
第一波先執行巨集任務
- JavaScript引擎在執行這段程式碼的時候,首先將全域性執行上下文壓入棧中,然後呢,在執行的時候會碰到
console.log
函式,將它壓入棧中,這個時候,直接執行console
函式,並輸出1
。然後console
函數出棧 - 繼續往下執行,碰到了
Promise.resolve().then()
,先將Promise.resolve().then()
壓入棧中,然後執行Promise.resolve().then()
,前面說過,這個then()
函式是個微任務,它會將傳入給它的回撥函式加入到微任務佇列中。然後Promise.resolve().then()
就出棧了。 - 接著執行,遇到
promise
的建構函式,這個建構函式是一個巨集任務,會直接將傳遞給它的函式壓入棧中。執行console
函式並輸出3
,執行完,console
函數出棧,接著執行resolve()
函式,並出棧。然後繼續執行then函式,將傳遞給then函式的引數函式放到微任務佇列中: - 繼續來,繼續往下執行。碰到
console.log(6)
,二話不說,直接壓入棧中,執行,輸出6
,出棧,一氣呵成。 - 接著,引擎碰到了
setTimeout
函式,這傢伙是個巨集任務,但同時它會將傳遞給它的函式,加入到任務佇列中:
好了,到此第一波巨集任務就全部執行完畢。接著,引擎就會去看一下微任務佇列中有沒有任務,如果有的話,執行它們。
第二波先執行微任務
-
現在看到的是,微任務佇列中有兩個任務。按照佇列的先入先出規則,先從
function () {console.log(2)}
開始執行。先是函式入棧,然後執行函式,輸出2
,然後函數出棧。接著執行下面這段程式碼:
console.log(4) setTimeout(function () { console.log(5) })
先從 console.log(4)
開始,先將它入棧,然後執行它,輸出 4
,然後函數出棧。
接著執行:
setTimeout(function () { console.log(5) })
上面說過 setTimeout
是巨集任務,加入到任務佇列中去。
繼續向下執行
function(){ Promise.resolve().then(function () { console.log(7) setTimeout(function () { console.log(8) }) }) }
這裡執行這個函式的時候遇到一個微任務,將這個微任務新增到微任務佇列,這一波又執行完了,接著就回去檢查微任務佇列中有沒有待執行的任務,一看還真有兩個小可愛等待執行,於是沒什麼好說的,直接擰出去就執行。
第三波
- 先是執行
console.log(7)
,然後輸出7
。接著執行setTimeout
,將傳遞給他的任務新增到任務佇列中去 - 最後就剩這兩個函數了,按照佇列的先入後出一次執行吧,輸出5和8。
最後看一下列印最後的結果就是 1,3,6,2,4,7,5,8
。你寫對了嗎?
4.總結
最後牢記兩點
Event Loop