如何快速入門VUE3.0:進入學習
關於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
first
執行上下文繼續執行,輸出Again first function
執行上下文全域執行上下文
全域執行上下文繼續執行,輸出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
所有微任務執行完畢後會再執行一個巨集任務,延遲
1000
毫秒的回呼函數會優先執行輸出setTimeout 1000
setTimeout 100
100
宏任務沒有產生微任務,所以沒有微任務需要執行
所以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渲染-> 巨集任務-> ...
【相關影片教學推薦:web前端】
以上就是深入淺析JavaScript中的執行上下文和執行機制的詳細內容,更多請關注php中文網其它相關文章!