關於js
中的執行上下文、執行棧、執行機制(同步任務、非同步任務、微任務、宏任務、事件迴圈)在面試中是一個高頻考點,有些小夥伴被問到時可能會一臉茫然不知所措,所以筆者今天就來總結下,希望可以對螢幕前的你有所幫助。【相關推薦:】
說js
中的執行上下文和js
執行機制之前我們來說說執行緒和程序
用官方的話術來說 執行緒
是CPU
排程的最小單位。
用官方的話術來說 程序
是CPU
資源分配的最小單位。
執行緒
是建立在程序
的基礎上的一次程式執行單位,通俗點解釋執行緒
就是程式中的一個執行流,一個程序
可以有一個或多個執行緒
。
一個程序
中只有一個執行流稱作單執行緒
,即程式執行時,所走的程式路徑按照連續順序排下來,前面的必須處理好,後面的才會執行。
一個程序
中有多個執行流稱作多執行緒
,即在一個程式中可以同時執行多個不同的執行緒
來執行不同的任務, 也就是說允許單個程式建立多個並行執行的執行緒
來完成各自的任務。
下面筆者舉一個簡單的例子,比如我們開啟qq音樂
聽歌,qq音樂
就可以理解為一個程序,在qq音樂
中我們可以邊聽歌邊下載這裡就是多執行緒,聽歌是一個執行緒,下載是一個執行緒。如果我們再開啟vscode
來寫程式碼這就是另外一個程序了。
程序之間相互獨立,但同一程序下的各個執行緒間有些資源是共用的。
執行緒的生命週期會經歷五個階段。
新建狀態: 使用 new
關鍵字和 Thread
類或其子類建立一個執行緒物件後,該執行緒物件就處於新建狀態。它保持這個狀態直到程式 start()
這個執行緒。
就緒狀態: 當執行緒物件呼叫了 start()
方法之後,該執行緒就進入就緒狀態。就緒狀態的執行緒處於就緒佇列中,只要獲得 CPU
的使用權就可以立即執行。
執行狀態: 如果就緒狀態的執行緒獲取 CPU
資源,就可以執行 run()
,此時執行緒便處於執行狀態。處於執行狀態的執行緒最為複雜,它可以變為阻塞狀態、就緒狀態和死亡狀態。
阻塞狀態: 如果一個執行緒執行了 sleep(睡眠)
、suspend(掛起)
、wait(等待)
等方法,失去所佔用資源之後,該執行緒就從執行狀態進入阻塞狀態。在睡眠時間已到或獲得裝置資源後可以重新進入就緒狀態。可以分為三種:
等待阻塞:執行狀態中的執行緒執行 wait()
方法,使執行緒進入到等待阻塞狀態。
同步阻塞:執行緒在獲取 synchronized
同步鎖失敗(因為同步鎖被其他執行緒佔用)。
其他阻塞:通過呼叫執行緒的 sleep()
或 join()
發出了 I/O
請求時,執行緒就會進入到阻塞狀態。當 sleep()
狀態超時,join()
等待執行緒終止或超時,或者 I/O
處理完畢,執行緒重新轉入就緒狀態。
死亡狀態: 一個執行狀態的執行緒完成任務或者其他終止條件發生時,該執行緒就切換到終止狀態。
JS
是單執行緒。JS
作為瀏覽器指令碼語言其主要用途是與使用者互動,以及操作DOM
。這決定了它只能是單執行緒,否則會帶來很複雜的同步問題。比如,假定JavaScript
同時有兩個執行緒,一個執行緒在某個DOM
節點上新增內容,另一個執行緒刪除了這個節點,這時瀏覽器應該以哪個執行緒為準?
當 JS
引擎解析到可執行程式碼片段(通常是函數呼叫階段)的時候,就會先做一些執行前的準備工作,這個 「準備工作」 ,就叫做 "執行上下文(execution context 簡稱 EC
)" 或者也可以叫做執行環境。
javascript
中有三種執行上下文型別,分別是:
全域性執行上下文 這是預設或者說是最基礎的執行上下文,一個程式中只會存在一個全域性上下文,它在整個 javascript
指令碼的生命週期內都會存在於執行堆疊的最底部不會被棧彈出銷燬。全域性上下文會生成一個全域性物件(以瀏覽器環境為例,這個全域性物件是 window
),並且將 this
值繫結到這個全域性物件上。
函數執行上下文 每當一個函數被呼叫時,都會建立一個新的函數執行上下文(不管這個函數是不是被重複呼叫的)。
Eval 函數執行上下文 執行在 eval
函數內部的程式碼也會有它屬於自己的執行上下文,但由於並不經常使用 eval
,所以在這裡不做分析。
前面我們說到js
在執行的時候會建立執行上下文,但是執行上下文是需要儲存的,那用什麼來儲存呢?就需要用到棧資料結構了。
棧是一種先進後出的資料結構。
所以總結來說用來儲存程式碼執行時建立的執行上下文就是執行棧。
在執行一段程式碼時,JS
引擎會首先建立一個執行棧,用來存放執行上下文。
然後 JS
引擎會建立一個全域性執行上下文,並 push
到執行棧中, 這個過程 JS
引擎會為這段程式碼中所有變數分配記憶體並賦一個初始值(undefined),在建立完成後,JS
引擎會進入執行階段,這個過程 JS
引擎會逐行的執行程式碼,即為之前分配好記憶體的變數逐個賦值(真實值)。
如果這段程式碼中存在 function
的呼叫,那麼 JS
引擎會建立一個函數執行上下文,並 push
到執行棧中,其建立和執行過程跟全域性執行上下文一樣。
當一個執行棧執行完畢後該執行上下文就會從棧中彈出,接下來會進入下一個執行上下文。
下面筆者來舉個例子,假如在我們的程式中有如下程式碼
console.log("Global Execution Context start"); function first() { console.log("first function"); second(); console.log("Again first function"); } function second() { console.log("second function"); } first(); console.log("Global Execution Context end");
上面的例子我們簡單來分析下
首先會建立一個執行棧
然後會建立一個全域性上下文,並將該執行上下文push
到執行棧中
開始執行,輸出Global Execution Context start
遇到first
方法,執行該方法,建立一個函數執行上下文並push
到執行棧
執行first
執行上下文,輸出first function
遇到second
方法,執行該方法,建立一個函數執行上下文並push
到執行棧
執行second
執行上下文,輸出second function
second
執行上下文執行完畢,從棧中彈出,進入到下一個執行上下文first
執行上下文
first
執行上下文繼續執行,輸出Again first function
first
執行上下文執行完畢,從棧中彈出,進入到下一個執行上下文全域性執行上下文
全域性執行上下文繼續執行,輸出Global Execution Context end
我們用一張圖來總結
好了。說完執行上下文和執行棧我們再來說說js
的執行機制
說到js
的執行機制,我們就需要了解js
中同步任務和非同步任務、宏任務和微任務了。
在js
中,任務分為同步任務和非同步任務,那什麼是同步任務什麼是非同步任務呢?
同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務。
非同步任務指的是,不進入主執行緒、而進入"任務佇列"的任務(任務佇列中的任務與主執行緒並列執行),只有當主執行緒空閒了並且"任務佇列"通知主執行緒,某個非同步任務可以執行了,該任務才會進入主執行緒執行。由於是佇列儲存所以滿足先進先出規則。常見的非同步任務有我們的setInterval
、setTimeout
、promise.then
等。
前面介紹了同步任務和非同步任務,下面我們來說說事件迴圈。
同步和非同步任務分別進入不同的執行"場所",同步的進入主執行緒,只有前一個任務執行完畢,才能執行後一個任務。非同步任務不進入主執行緒而是進入 Event Table
並註冊函數。
當指定的事情完成時,Event Table
會將這個函數移入 Event Queue
。Event Queue
是佇列資料結構,所以滿足先進先出規則。
主執行緒內的任務執行完畢為空,會去 Event Queue
讀取對應的函數,進入主執行緒執行。
上述過程會不斷重複,也就是常說的 Event Loop(事件迴圈)。
我們用一張圖來總結下
下面筆者簡單來介紹個例子
function test1() { console.log("log1"); setTimeout(() => { console.log("setTimeout 1000"); }, 1000); setTimeout(() => { console.log("setTimeout 100"); }, 100); console.log("log2"); } test1(); // log1、log2、setTimeout 100、setTimeout 1000
我們知道在js中會優先執行同步任務再執行非同步任務,所以上面的例子會先輸出log1、log2
同步任務執行完後會執行非同步任務,所以延遲100
毫秒的回撥函數會優先執行輸出setTimeout 100
延遲1000
毫秒的回撥函數會後執行輸出setTimeout 1000
上面的例子比較簡單,相信只要你看懂了上面筆者說的同步非同步任務做出來是沒什麼問題的。那下面筆者再舉一個例子小夥伴們看看會輸出啥呢?
function test2() { console.log("log1"); setTimeout(() => { console.log("setTimeout 1000"); }, 1000); setTimeout(() => { console.log("setTimeout 100"); }, 100); new Promise((resolve, reject) => { console.log("new promise"); resolve(); }).then(() => { console.log("promise.then"); }); console.log("log2"); } test2();
要解決上面的問題光知道同步和非同步任務是不夠的,我們還得知道宏任務和微任務。
在js
中,任務被分為兩種,一種叫宏任務MacroTask
,一種叫微任務MicroTask
。
常見的宏任務MacroTask
有
主程式碼塊
setTimeout()
setInterval()
setImmediate() - Node
requestAnimationFrame() - 瀏覽器
常見的微任務MicroTask
有
Promise.then()
process.nextTick() - Node
所以在上面的例子中就涉及到宏任務和微任務了,那宏任務微任務的執行順序是怎麼樣的呢?
首先,整體的 script
(作為第一個宏任務)開始執行的時候,會把所有程式碼分為同步任務、非同步任務兩部分,同步任務會直接進入主執行緒依次執行,非同步任務會進入非同步佇列然後再分為宏任務和微任務。
宏任務進入到 Event Table
中,並在裡面註冊回撥函數,每當指定的事件完成時,Event Table
會將這個函數移到 Event Queue
中
微任務也會進入到另一個 Event Table
中,並在裡面註冊回撥函數,每當指定的事件完成時,Event Table
會將這個函數移到 Event Queue
中
當主執行緒內的任務執行完畢,主執行緒為空時,會檢查微任務的 Event Queue
,如果有任務,就全部執行,如果沒有就執行下一個宏任務
我們用一張圖來總結下
讀懂了非同步裡面的宏任務和微任務上面的例子我們就可以輕易的得到答案了。
我們知道在js中會優先執行同步任務再執行非同步任務,所以上面的例子會先輸出log1、new promise、log2
。這裡需要注意new promise裡面是同步的
主程式碼塊作為宏任務執行完後會執行此宏任務所產生的所有微任務,所以會輸出promise.then
所有微任務執行完畢後會再執行一個宏任務,延遲100
毫秒的回撥函數會優先執行輸出setTimeout 100
此宏任務沒有產生微任務,所以沒有微任務需要執行
繼續執行下一個宏任務,延遲1000
毫秒的回撥函數會優執行輸出setTimeout 1000
所以test2方法執行後會依次輸出log1、new promise、log2、promise.then、setTimeout 100、setTimeout 1000
關於
js
執行到底是先宏任務再微任務還是先微任務再宏任務網上的文章各有說辭。筆者的理解是如果把整個js
程式碼塊當做宏任務的時候我們的js
執行順序是先宏任務後微任務的。
正所謂百看不如一練,下面筆者舉兩個例子如果你都能做對那你算是掌握了js
執行機制這一塊的知識了。
例子1
function test3() { console.log(1); setTimeout(function () { console.log(2); new Promise(function (resolve) { console.log(3); resolve(); }).then(function () { console.log(4); }); console.log(5); }, 1000); new Promise(function (resolve) { console.log(6); resolve(); }).then(function () { console.log(7); setTimeout(function () { console.log(8); }); }); setTimeout(function () { console.log(9); new Promise(function (resolve) { console.log(10); resolve(); }).then(function () { console.log(11); }); }, 100); console.log(12); } test3();
我們來具體分析下
首先js
整體程式碼塊作為一個宏任務最開始執行,依次輸出1、6、12
。
整體程式碼塊宏任務執行完畢後產生了一個微任務和兩個宏任務,所以宏任務佇列有兩個宏任務,微任務佇列有一個微任務。
宏任務執行完畢後會執行此宏任務所產生的的所有微任務。因為只有一個微任務,所以會輸出7
。此微任務又產生了一個宏任務,所以宏任務佇列目前有三個宏任務。
三個宏任務裡面沒有設定延遲的最先執行,所以輸出8
,此宏任務沒有產生微任務,所以沒有微任務要執行,繼續執行下一個宏任務。
延遲100
毫秒的宏任務執行,輸出9、10
,併產生了一個微任務,所以微任務佇列目前有一個微任務
宏任務執行完畢後會執行該宏任務所產生的所有微任務,所以會執行微任務佇列的所有微任務,輸出11
延遲1000
毫秒的宏任務執行輸出2、3、5
,併產生了一個微任務,所以微任務佇列目前有一個微任務
宏任務執行完畢後會執行該宏任務所產生的所有微任務,所以會執行微任務佇列的所有微任務,輸出4
所以上面程式碼例子會依次輸出1、6、12、7、8、9、10、11、2、3、5、4
,小夥伴們是否做對了呢?
例子2
我們把上面的例子1稍作修改,引入async
和await
async function test4() { console.log(1); setTimeout(function () { console.log(2); new Promise(function (resolve) { console.log(3); resolve(); }).then(function () { console.log(4); }); console.log(5); }, 1000); new Promise(function (resolve) { console.log(6); resolve(); }).then(function () { console.log(7); setTimeout(function () { console.log(8); }); }); const result = await async1(); console.log(result); setTimeout(function () { console.log(9); new Promise(function (resolve) { console.log(10); resolve(); }).then(function () { console.log(11); }); }, 100); console.log(12); } async function async1() { console.log(13) return Promise.resolve("Promise.resolve"); } test4();
上面這裡例子會輸出什麼呢?這裡我們弄懂async
和await
題目就迎刃而解了。
我們知道async
和await
其實是Promise
的語法糖,這裡我們只需要知道await
後面就相當於Promise.then
。所以上面的例子我們可以理解成如下程式碼
function test4() { console.log(1); setTimeout(function () { console.log(2); new Promise(function (resolve) { console.log(3); resolve(); }).then(function () { console.log(4); }); console.log(5); }, 1000); new Promise(function (resolve) { console.log(6); resolve(); }).then(function () { console.log(7); setTimeout(function () { console.log(8); }); }); new Promise(function (resolve) { console.log(13); return resolve("Promise.resolve"); }).then((result) => { console.log(result); setTimeout(function () { console.log(9); new Promise(function (resolve) { console.log(10); resolve(); }).then(function () { console.log(11); }); }, 100); console.log(12); }); } test4();
看到上面的程式碼是不是就能輕易得出結果呢?
首先js
整體程式碼塊作為一個宏任務最開始執行,依次輸出1、6、13
。
整體程式碼塊宏任務執行完畢後產生了兩個微任務和一個宏任務,所以宏任務佇列有一個宏任務,微任務佇列有兩個微任務。
宏任務執行完畢後會執行此宏任務所產生的的所有微任務。所以會輸出7、Promise.resolve、12
。此微任務又產生了兩個宏任務,所以宏任務佇列目前有三個宏任務。
三個宏任務裡面沒有設定延遲的最先執行,所以輸出8
,此宏任務沒有產生微任務,所以沒有微任務要執行,繼續執行下一個宏任務。
延遲100
毫秒的宏任務執行,輸出9、10
,併產生了一個微任務,所以微任務佇列目前有一個微任務
宏任務執行完畢後會執行該宏任務所產生的所有微任務,所以會執行微任務佇列的所有微任務,輸出11
延遲1000
毫秒的宏任務執行輸出2、3、5
,併產生了一個微任務,所以微任務佇列目前有一個微任務
宏任務執行完畢後會執行該宏任務所產生的所有微任務,所以會執行微任務佇列的所有微任務,輸出4
所以上面程式碼例子會依次輸出1、6、13、7、Promise.resolve、12、8、9、10、11、2、3、5、4
,小夥伴們是否做對了呢?
關於setTimeout(fn)
可能很多小夥伴還是不太理解,這不明明沒設定延遲時間嗎,不應該立即就執行嗎?
setTimeout(fn)
我們可以理解成setTimeout(fn,0)
,其實是同一個意思。
我們知道js分同步任務和非同步任務,setTimeout(fn)
就是屬於非同步任務,所以這裡就算你沒設定延遲時間,他也會進入非同步佇列,需要等到主執行緒空閒的時候才會執行。
筆者這裡再提一嘴,你覺得我們在setTimeout
後面設定的延遲時間,js
就一定會按我們的延遲時間執行嗎,我覺得並不見得。我們設定的時間只是該回撥函數可以被執行了,但是主執行緒有沒有空還是另外一回事,我們可以舉個簡單的例子。
function test5() { setTimeout(function () { console.log("setTimeout"); }, 100); let i = 0; while (true) { i++; } } test5();
上面的例子一定會在100
毫秒後輸出setTimeout
嗎,並不會,因為我們的主執行緒進入了死迴圈,並沒有空去執行非同步佇列的任務。
GUI渲染
在這裡說有些小夥伴可能不太理解,後面筆者會出關於瀏覽器的文章會再詳細介紹,這裡只是簡單瞭解下即可。
由於JS引擎執行緒
和GUI渲染執行緒
是互斥的關係,瀏覽器為了能夠使宏任務
和DOM任務
有序的進行,會在一個宏任務
執行結果後,在下一個宏任務
執行前,GUI渲染執行緒
開始工作,對頁面進行渲染。
所以宏任務、微任務、GUI渲染之間的關係如下
宏任務 -> 微任務 -> GUI渲染 -> 宏任務 -> ...
【相關視訊教學推薦:】
以上就是深入淺析JavaScript中的執行上下文和執行機制的詳細內容,更多請關注TW511.COM其它相關文章!