JavaScript運行機制

一、前言

大家都知道JavaScript是單線程的,單線程就意味著同一時間只能做一件事,那么有同學會問,為什么JavaScript的作者不把它設計成多線程的呢,那樣性能不是更好。為了回答這個問題,我們得從JavaScript的用途上來解釋了,由于JavaScript是一門腳本語言,被用于與用戶進行交互和操作DOM有關,如果是多線程的話, 會出現很多復雜的同步問題,讓JavaScript的操作變得難以控制。假如現在有一個線程A在dom上新增一個節(jié)點a,另一個線程又在dom上刪除了節(jié)點a,那么我們該以哪個線程為標準呢。所以,對于JavaScript單線程這一特點,未來也不會改變。對于一些JavaScript開發(fā)者來說,JavaScript的運行機制一直困擾著一些同學,比如異步請求的執(zhí)行問題,為什么js代碼會造成頁面渲染的阻塞,作用域中的變量提升等等到底做了什么,看完下面的文章你應該會對這些問題有清楚的了解。

二、進程與線程

我們經常說,JavaScript是單線程的,那到底什么是線程呢。官方的說法是,進程是CPU資源分配的最小單位,而線程是CPU調度的最小單位。

大家看到這句話可能有些懵。那以瀏覽器為例,當我們在瀏覽器中打開一個新的標簽頁Tab的時候,CPU會為瀏覽器分配一個新的進程,去渲染我們的網頁,而渲染網頁的工作是通過這個進程中的多個線程來配合完成的,包括瀏覽器的渲染線程、JS引擎線程、http異步請求線程等等。

所以,一個進程由多個線程組成,每個線程是進程的不同執(zhí)行路線。而進程與進程之間是相對獨立的,如:在瀏覽器打開兩個標簽頁Tab,就是兩個進程,這兩個標簽頁的運行是互不影響的。

三、瀏覽器內核

說到瀏覽器內核,就不得不提到五大主流瀏覽器:

  • IE(IE內核),

  • Chrome瀏覽器(以前是Webkit內核,現在是Blink內核),

  • Safari(Webkit內核),

  • Firefox(Gecko內核),

  • Opera(最開始是Pestro內核,然后是Webkit內核,最后是Blink內核),

也正是因為不同瀏覽器的內核不同,導致有些相同html元素在不同瀏覽器上的表現不同,這主要是由于瀏覽器內核中的GUI渲染線程不同所導致。

瀏覽器內核是多線程的,在內核的控制下,多個線程相互配合以保持同步,一個瀏覽器內核通常由以下幾個線程組成:

  • GUI渲染線程

  • JS引擎線程

  • 定時器觸發(fā)線程

  • 事件觸發(fā)線程

  • 異步HTTP請求線程

1、GUI渲染線程

該線程主要負責解析HTML,CSS,構建DOM樹,布局和繪制等

當頁面需要重繪或者引起回流時,將會執(zhí)行該線程

注意,該線程是與JS引擎線程互斥的,當執(zhí)行JS引擎線程時,GUI渲染線程將會被掛起(凍結),等到任務隊列為空的時候,主線程才會去執(zhí)行GUI渲染線程

2、JS引擎線程

主要負責處理JavaScript腳本,執(zhí)行代碼

也負責執(zhí)行準備好執(zhí)行的事件,如定時器計時結束或異步請求成功并正確返回時,將依次進入任務隊列,等待JS引擎線程的執(zhí)行

當然,該線程是與GUI渲染引擎線程互斥的,當JS引擎執(zhí)行JavaScript代碼時間過長時會造成頁面的阻塞,也就是為什么我們要把script標簽在body的最后面引入

3、定時器觸發(fā)線程

負責執(zhí)行定時器一類函數的進程,如settimeout、setInterval

當主線程依次執(zhí)行代碼時,遇到計時器,會將計時器交給該線程處理,當計時完畢之后,定時器觸發(fā)線程會將計時完畢后的事件加入到事件隊列的尾部,等待JS引擎線程的執(zhí)行

4、事件觸發(fā)線程

主要負責將準備好執(zhí)行的事件交給JS引擎線程執(zhí)行,如計時器計時完畢后的事件,AJAX請求成功返回并觸發(fā)的回調函數和用戶觸發(fā)點擊事件時,事件觸發(fā)線程會將回調函數加入到任務隊列的尾部,等待JS引擎線程的執(zhí)行

5、異步HTTP請求線程

負責執(zhí)行異步請求一類的函數,如ajax,fetch,axios等

當主線程依次執(zhí)行代碼時,遇到異步請求,會將函數交給該線程處理,當監(jiān)聽狀態(tài)碼變更時,如果有回調函數,會將回調函數加入到任務隊列的尾部,等待JS引擎線程的執(zhí)行

四、任務隊列

單線程就意味著,所有任務的執(zhí)行都需要排隊,前一個任務結束,后一個任務才能執(zhí)行,如果一個任務耗時很長,后一個任務不得不一直等待著。

JavaScript的作者意識到這個問題,將所有任務劃分為兩種,一種是同步任務,一種是異步任務。

同步任務是指在主線程上排隊執(zhí)行的任務,前一個任務結束,后一個任務才能執(zhí)行。

異步任務是指不進入主線程執(zhí)行,而進入“任務隊列”的任務,只有當“任務隊列”通知主線程可以執(zhí)行了,該任務才會進入主線程。

異步任務分為兩種,宏任務和微任務(后面會重點介紹)。接下來通過兩個例子來說明同步任務和異步任務的主要區(qū)別:

console.log('a')

while (true) {

    console.log('這里是while')

}

console.log('b')

最后打印的結果是a,還有無數個'這里是while',直到頁面卡死,因為上述代碼均屬于同步任務,由上到下依次執(zhí)行,當主線程執(zhí)行完console.log('a')之后,開始執(zhí)行while循環(huán),死循環(huán)導致調用棧會一直執(zhí)行while循環(huán)中代碼,阻塞了while循環(huán)以后的代碼執(zhí)行,導致while循環(huán)后面的任務就無法執(zhí)行了。

console.log('a')

settimeout(function () {

    console.log('settimeout1')

},0)

while (true) {

    console.log('這里是while')

} 

最后的打印結果還是a,還有無數個'這里是while',直到頁面卡死,因為這段代碼中同時存在同步任務和異步任務,異步任務要等到主線程上所有的同步任務執(zhí)行完成之后才能執(zhí)行。上述代碼中的console.log('a')和while循環(huán)均屬于同步任務,而settimeout屬于異步任務(在后面的事件循環(huán)中會介紹哪些事件屬于異步任務),所以當執(zhí)行完console.log('a')之后,主線程將執(zhí)行while循環(huán),死循環(huán)導致調用棧會一直執(zhí)行while循環(huán)中代碼,阻塞了while循環(huán)以后的代碼執(zhí)行無法執(zhí)行下面的代碼了

五、事件循環(huán)(Event Loop)

下圖為一個完整的事件循環(huán)的過程:


image.png

事件循環(huán)的運行機制:

一開始執(zhí)行???我們可以把執(zhí)行棧認為是一個存儲函數調用的棧結構,遵循先進后出的原則。微任務隊列空,宏任務隊列里有且只有一個script腳本(整體代碼)。

全局上下文(script 標簽)被推入執(zhí)行棧,同步代碼執(zhí)行。在執(zhí)行的過程中,會判斷是同步任務還是異步任務,通過對一些接口的調用,可以產生新的宏任務與微任務,它們會分別被推入各自的任務隊列里。同步代碼執(zhí)行完了,全局script腳本會被移出宏隊列,這個過程本質上是隊列的宏任務的執(zhí)行和出隊的過程。

上一步我們出隊的是一個宏任務,這一步我們處理的是微任務。但需要注意的是:當宏任務出隊時,任務是一個一個執(zhí)行的;而微任務出隊時,任務是一隊一隊執(zhí)行的。因此,我們處理微任務隊列這一步,會逐個執(zhí)行隊列中的任務并把它出隊,直到隊列被清空。

主線程從“任務隊列”讀取事件這個過程,是循環(huán)不斷的,所以整個這種運行機制就叫做Event Loop(事件循環(huán))。每當主線程為空時,就會去讀取“事件隊列”,這就是JavaScript的運行機制

六、宏任務(Macrotask)和微任務(Microtask)

我們在上面提到,異步任務分為宏任務和微任務:

宏任務包括:全局script任務、setTimeout、setInterval、setImmediate、I/O操作、UI rendering

微任務包括:new Promise.then()、MutationObserver(HTML5新特性)等

當主線程上的所有同步任務執(zhí)行完之后,是先執(zhí)行宏任務還是先執(zhí)行微任務呢?

由于代碼入口都是全局任務script,而全局任務script屬于宏任務,所以當棧為空或者同步代碼執(zhí)行完之后,會先執(zhí)行微任務隊列里的任務

當微任務隊列里的所有任務都執(zhí)行完成之后,主線程會讀取宏任務最前面的任務

執(zhí)行宏任務的過程中,遇到微任務,依次加入微任務隊列

當前主線程上的調用棧為空時,再次讀取微任務隊列的任務,以此類推


image.png

以下通過一個例子來理解異步任務的運行機制:

Promise.resolve().then(() => {

    console.log('Promse1')

    setTimeout(function () {

        console.log('setTimeout1')

    }, 0)

})

setTimeout(function () {

    console.log('setTimeout2')

    Promise.resolve().then(() => {

        console.log('Promise2')

    })

}, 0)

最終打印結果依次為Promise1、setTimeout2、Promise2、setTimeout1

一開始執(zhí)行棧所有的同步任務執(zhí)行完成,主線程會去讀取微任務隊列(此時微任務隊列有且只有一個微任務),執(zhí)行微任務中的任務打印出Promise1,同時也會生成一個宏任務setTimeout1

當執(zhí)行棧為空時,主線程又會去讀取宏任務隊列最前面的任務。此時,宏任務隊列依次排列著[setTimeou2, setTimeout1],所以setTimeout2執(zhí)行打印setTimeout2,同時生成一個微任務Promise2加入微任務隊列

當主線程執(zhí)行完宏任務setTimeout2之后,調用棧為空,去讀取微任務隊列,此時,微任務隊列只有一個微任務Promise2,執(zhí)行微任務中的任務打印Promise2

當主線程執(zhí)行完微任務Promise2之后,調用棧為空,去讀取宏任務隊列,此時,宏任務隊列就只剩下setTimeout1了,執(zhí)行setTimeout1打印setTimeout1。

?著作權歸作者所有,轉載或內容合作請聯系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • 一、JavaScript單線程模型 JavaScript是單線程的,JavaScript只在一個線程上運行,但是瀏...
    Brolly閱讀 1,236評論 4 6
  • JavaScript單線程機制 JavaScript的一個語言特性(也是這門語言的核心)就是單線程。什么是單線程呢...
    October_yang閱讀 584評論 0 1
  • 一、引子 本文介紹JavaScript運行機制,這一部分比較抽象,我們先從一道面試題入手: 這一題看似很簡單,但如...
    浪里行舟閱讀 713評論 0 12
  • 原文鏈接: 深入淺出JavaScript運行機制 一、引子 一道面試題入手: 請問數字打印順序是什么?題目的答案是...
    alanwhy閱讀 580評論 0 0
  • 今天居然醒來的可早,凌晨3點就睡醒了,躺床上也是翻來覆去睡不著,正好夜讀看書,靜靜的夜,捧著書細細滴讀,如水的夜,...

友情鏈接更多精彩內容