首頁  >  文章  >  web前端  >  技術解答之JavaScript的執行機制

技術解答之JavaScript的執行機制

WBOY
WBOY轉載
2022-01-14 17:23:171348瀏覽

這篇文章帶大家帶來了JavaScript中執行機制的相關問題,不論是工作還是面試,我們可能都常常會碰到需要知道程式碼的執行順序的場景,希望對大家有幫助。

技術解答之JavaScript的執行機制

程式與執行緒

我們都知道電腦的核心是CPU,它承擔了所有的計算任務;而作業系統是電腦的管理者,它負責任務的調度、資源的分配和管理,統領整個電腦硬體;應用程式則是具有某種功能的程序,程式是運行於作業系統之上的。

進程

進程是一個具有獨立功能的程式在一個資料集上的一次動態執行的過程,是操作系統進行資源分配和調度的一個獨立單位,是應用程式運作的載體程序是能擁有資源和獨立運作的最小單位,也是程式執行的最小單位。

進程具有的特徵:

  • 動態:進程是程式的一次執行過程,是暫時的,有生命期的,是動態產生,動態消亡的;

  • 並發性:任何進程都可以與其他進程一起並發執行;

  • 獨立性:進程是系統進行資源分配和調度的一個獨立單位;

  • 結構性:進程由程式、資料和進程控制區塊三部分組成。

執行緒

#執行緒是程式執行中單一的順序控制流程,是程式執行流的最小單元,是處理器調度和分派的基本單位。一個行程可以有一個或多個線程,各個線程之間共享程式的記憶體空間(也就是所在行程的記憶體空間)。一個標準的執行緒由執行緒ID、目前指令指標(PC)、暫存器和堆疊組成。而該行程由記憶體空間(程式碼、資料、行程空間、開啟的檔案)和一個或多個執行緒組成。

處理程序與執行緒的差異

一個行程由一個或多個執行緒組成,執行緒是一個行程中程式碼的不同執行路線;

#行程之間相互獨立,但同一行程下的各個執行緒之間共享程式的記憶體空間(包括程式碼段、資料集、堆等)及一些行程級的資源(如開啟檔案與訊號),程序與程序之間互不可見;

  • #調度和切換:線程上下文切換比進程上下文切換快得多。
  • JS為什麼是單執行緒?

  • JavaScript從它誕生之初就是作為瀏覽器的腳本語言,主要用來處理使用者互動以及操作DOM,這就決定了它只能是單線程的,否則會帶來非常複雜的同步問題。

  • 舉個例子: 如果JS是多執行緒的,其中一個執行緒要修改一個DOM元素,另外一個執行緒想要刪除這個DOM元素,這時候瀏覽器就不知道該聽誰的。所以為了避免複雜性,從一誕生,JavaScript就被設計成單一執行緒。
  • 為了利用多核心CPU的運算能力,HTML5提出Web Worker標準,允許JavaScript腳本建立多個線程,但是子執行緒完全受主執行緒控制,且不得操作DOM。所以,這個新標準並沒有改變JavaScript單執行緒的本質

  • 瀏覽器原理

  • 作為前端工程師,瀏覽器想必都不陌生,並且瀏覽器是多進程的。

瀏覽器組成部分

#使用者介面:包含網址列,前進/後退/刷新/書籤

瀏覽器引擎:在使用者介面與呈現引擎之間傳送指令

  • #渲染引擎:用來繪製要求的內容

    ### #######網路:用來完成網路調用,例如http請求,它具有平台無關的接口,可以在不同平台上工作############JavaScript解釋器:用來解析執行JavaScript程式碼############使用者介面後端:用於繪製基本的視窗小工具,例如組合方塊和窗口,底層使用作業系統的使用者介面######## #####資料儲存:屬於持久層,瀏覽器在硬碟中保存類似cookie的各種數據,HTML5定義了web database技術,這是一種輕量級完整的客戶端儲存技術##### #######注意:與大多數瀏覽器不同的是,Google(Chrome)瀏覽器的每個標籤頁都分別對應一個呈現引擎實例。每個標籤頁都是一個獨立的進程############瀏覽器包含哪些進程###############瀏覽器進程##### ##########瀏覽器的主程序(負責協調、主控),該程序只有一個###
  • 負責瀏覽器介面顯示,與使用者互動。如前進,後退等

  • 負責各個頁面的管理,創建和銷毀其他進程

  • 將渲染(Renderer)進程得到的記憶體中的Bitmap(點陣圖),繪製到使用者介面上

  • 網路資源的管理,下載等

第三方外掛程式

負責管理第三方外掛程式

GPU行程

#負責3D繪圖與硬體加速(最多一個)

#渲染程式

負責頁面文件解析,執行與渲染

#渲染程式包含哪些執行緒

GUI渲染執行緒

主要負責解析HTML,CSS,建構DOM樹,佈局,繪製等

該執行緒與JavaScript引擎執行緒互斥,當執行JavaScript引擎執行緒時,GUI渲染執行緒會被掛起,當任務佇列空閒時,主執行緒才會執行GUI渲染

#JavaScript引擎執行緒

#主要負責處理JavaScript腳本,執行程式碼(如V8引擎)

瀏覽器同時只能有一個JS引擎執行緒在執行JS程序,即JS是單執行緒的

JS引擎執行緒與GUI渲染執行緒是互斥的,所以JS引擎會阻塞頁面渲染

定時觸發器執行緒

負責執行定時器函數(setTimeout,setInterval)

#瀏覽器定時計數器並不是由JS引擎計數的(因為JS是單線程的,如果處於阻塞狀態就會影響計數器的準確性)

通過單獨線程來計時並觸發定時(計時完畢後,添加到事件觸發線程的事件佇列中,等待JS引擎空閒後執行),這個線程就是定時觸發器線程,也叫定時器線程

W3C在HTML標準中規定,規定要求setTimeout中低於4ms的時間間隔算為4ms

事件觸發執行緒

負責將準備好的事件交給JS引擎執行緒執行

當事件被觸發時,這個執行緒會把對應的事件加到待處理佇列的隊尾,等待JS引擎處理

非同步請求執行緒

在XMLHttpRequest連線後瀏覽器會開一個當執行緒

偵測請求狀態變更時,如果有對應的回呼函數,非同步請求執行緒就會產生狀態變更事件,並把對應的回呼函數放入佇列中等待JS引擎執行

同步與非同步

由於JavaScript是單執行緒的,這就決定了它的任務不可能只有同步任務,那些耗時很長的任務如果也依同步任務執行的話將會導致頁面阻塞,所以JavaScript任務一般分為兩類:

同步任務

同步任務指的是,在主執行緒上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;

非同步任務

異步任務指的是,不進入主執行緒、而進入"任務隊列"(Event queue)的任務,只有"任務隊列"通知主線程,某個非同步任務可以執行了,該任務才會進入主執行緒執行。

常見的非同步任務: 定時器,ajax,事件綁定,回呼函數,promise,async await等

  • 同步和非同步任務分別進入不同的執行"場所",同步的進入主線程,非同步的進入Event Table並註冊函數。

  • 當Event Table中指定的事情完成時,會將這個函數移入Event Queue。

  • 主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函數,進入主執行緒執行。

  • 上述過程會不斷重複,也就是常說的Event Loop(事件循環)。

  • 我們不禁要問了,那怎麼知道主執行緒執行棧為空啊? js引擎存在monitoring process進程,會持續不斷的檢查主執行緒執行堆疊是否為空,一旦為空,就會去Event Queue檢查是否有等待被呼​​叫的函數。

巨集任務與微任務

#JavaScript除了廣義上的同步任務與非同步任務,還有更精細的任務定義:

  • 巨集任務(macro-task): 包含全域程式碼,setTimeout,setInterval

  • ##微任務(micro-task) : new Promise().then(回呼) process.nextTick()

#不同類型的任務會進入到不同的任務佇列:

技術解答之JavaScript的執行機制

#事件循環的順序,決定js程式碼的執行順序。進入整體程式碼(巨集任務)後,開始第一次迴圈。接著執行所有的微任務。然後再次從巨集任務開始,找到其中一個任務佇列執行完畢,然後再執行所有的微任務。

執行堆疊與任務佇列

#執行堆疊

JavaScript程式碼都是在執行上下文中執行的,在JavaScript中有三種執行上下文:

  • #全域執行上下文

  • ##函數執行上下文,JS函數被呼叫時都會建立一個函數執行上下文

  • eval執行上下文,eval函數產生的上下文(用的較少)

#通常來說我們的JS程式碼不只一個上下文,那麼這些上下文的執行順序是怎麼樣的呢?

我們都知道堆疊是一種後進先出的資料結構,我們JavaScript中的執行堆疊就是一種這樣的堆疊結構,當JS引擎執行程式碼時,會產生一個全域上下文並把它壓入執行棧,每當遇到函數呼叫時,就會產生函數執行上下文並壓入執行棧。引擎從棧頂開始執行函數,執行完後會彈出該執行上下文。

function add(){
  console.log(1)
  foo()
  console.log(3)
}
function foo(){
  console.log(2)
}
add()

我們來看下上面這段程式碼的執行堆疊是怎樣的:

技術解答之JavaScript的執行機制

任務佇列

#前面我們說到了JavaScript中所有的任務分為同步任務與非同步任務,同步任務,顧名思義就是立即執行的任務,它一般是直接進入到主執行緒中執行。而我們的非同步任務則是進入任務佇列等待主執行緒中的任務執行完再執行。

任務佇列是一個事件的佇列,表示相關的非同步任務可以進入執行堆疊了。主執行緒讀取任務佇列就是讀取裡面有哪些事件。

佇列是一種先進先出的資料結構。

上面我們說到非同步任務又可以分成巨集任務與微任務,所以任務佇列也可以分成巨集任務佇列與微任務佇列

  • Macrotask Queue :進行比較大型的工作,常見的有setTimeout,setInterval,使用者互動操作,UI渲染等;

  • Microtask Queue:進行較小的工作,常見的有Promise,Process. nextTick;

事件循環(Event-Loop)

同步任務直接放入主執行緒執行,非同步任務(點選事件,計時器,ajax等)掛在後台執行,等待I/O事件完成或行為事件被觸發。

系統後台執行非同步任務,如果某個非同步任務事件(或行為事件被觸發),則將該任務新增至任務佇列,並且每個任務會對應一個回呼函數進行處理。

這裡非同步任務分為巨集任務與微任務,巨集任務進入到巨集任務佇列,微任務進入到微任務佇列。

執行任務佇列中的任務具體是在執行堆疊中完成的,當主執行緒中的任務全部執行完畢後,去讀取微任務佇列,如果有微任務就會全部執行,然後再去讀取巨集任務佇列

上述過程會不斷的重複進行,也就是我們常說的事件循環(Event-Loop)。

技術解答之JavaScript的執行機制

範例驗證

我們來看題目進行驗證

(async ()=>{
    console.log(1) 
  
    setTimeout(() => {
    console.log('setTimeout1')
    }, 0);
  
    function foo (){
        return new Promise((res,rej) => {
            console.log(2)
            res(3)
        })
    }
  
    new Promise((resolve,reject)=>{
    console.log(4)
    resolve() 
    console.log(5)
    }).then(()=> {
    console.log('6')
    })
  
    const res = await foo();
    console.log(res);
    console.log('7')
  
    setTimeout(_ => console.log('setTimeout2'))
})()

列印順序是:1,4,5,2,6 ,3,7,setTimeout1,setTimeout2

分析:

程式碼自上而下執行,先遇到console.log(1),直接列印1,接著遇到定時器屬於宏任務,放入巨集任務佇列

再遇到promise,由於new Promise是同步任務,所以直接列印4,遇到resolve,也就是後面的then函數,放入微任務佇列,再列印5

然後再執行await foo,foo函數裡面有個promise,new promise屬於同步任務,所以會直接列印2,await回傳的是一個promise的回調,await後面的任務放入微任務佇列

最後遇到一個定時器,放入巨集任務佇列

執行堆疊任務執行完了,先去微任務佇列取得微任務執行,先執行第一個微任務,列印6 ,再執行第二個微任務,列印3,7

微任務執行完,再去巨集任務佇列取得巨集任務執行,列印setTimeout1,setTimeout2

【相關推薦:

javascript學習教學

以上是技術解答之JavaScript的執行機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述:
本文轉載於:juejin.im。如有侵權,請聯絡admin@php.cn刪除