原來JavaScript是這樣運行的

1、前言

你們都知道JavaScript是單線程的,單線程就意味着同一時間只能作一件事,那麼有同窗會問,爲何JavaScript的做者不把它設計成多線程的呢,那樣性能不是更好。爲了回答這個問題,咱們得從JavaScript的用途上來解釋了,因爲JavaScript是一門腳本語言,被用於與用戶進行交互和操做DOM有關,若是是多線程的話, 會出現不少複雜的同步問題,讓JavaScript的操做變得難以控制。假如如今有一個線程A在dom上新增一個節點a,另外一個線程又在dom上刪除了節點a,那麼咱們該以哪一個線程爲標準呢。因此,對於JavaScript單線程這一特色,將來也不會改變。對於一些JavaScript開發者來講,JavaScript的運行機制一直困擾着一些同窗,好比異步請求的執行問題,爲何js代碼會形成頁面渲染的阻塞,做用域中的變量提高等等到底作了什麼,看完下面的文章你應該會對這些問題有清楚的瞭解。javascript

2、進程與線程

咱們常常說,JavaScript是單線程的,那到底什麼是線程呢。官方的說法是,進程是CPU資源分配的最小單位,而線程是CPU調度的最小單位。你們看到這句話可能有些懵。那以瀏覽器爲例,當咱們在瀏覽器中打開一個新的標籤頁Tab的時候,CPU會爲瀏覽器分配一個新的進程,去渲染咱們的網頁,而渲染網頁的工做是經過這個進程中的多個線程來配合完成的,包括瀏覽器的渲染線程、JS引擎線程、http異步請求線程等等。因此,一個進程由多個線程組成,每一個線程是進程的不一樣執行路線。而進程與進程之間是相對獨立的,如:在瀏覽器打開兩個標籤頁Tab,就是兩個進程,這兩個標籤頁的運行是互不影響的。
html

3、瀏覽器內核

說到瀏覽器內核,就不得不提到五大主流瀏覽器IE(IE內核),Chrome瀏覽器(之前是Webkit內核,如今是Blink內核),Safari(Webkit內核),Firefox(Gecko內核),Opera(最開始是Pestro內核,而後是Webkit內核,最後是Blink內核),也正是由於不一樣瀏覽器的內核不一樣,致使有些相同html元素在不一樣瀏覽器上的表現不一樣,這主要是因爲瀏覽器內核中的GUI渲染線程不一樣所致使。
java

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

一、GUI渲染線程
二、JS引擎線程
三、定時器觸發線程
四、事件觸發線程
五、異步HTTP請求線程ajax

一、GUI渲染線程

  • 該線程主要負責解析HTML,CSS,構建DOM樹,佈局和繪製等
  • 當頁面須要重繪或者引發迴流時,將會執行該線程
  • 注意,該線程是與JS引擎線程互斥的,當執行JS引擎線程時,GUI渲染線程將會被掛起(凍結),等到任務隊列爲空的時候,主線程纔會去執行GUI渲染線程

二、JS引擎線程

  • 主要負責處理JavaScript腳本,執行代碼
  • 也負責執行準備好執行的事件,如定時器計時結束或異步請求成功並正確返回時,將依次進入任務隊列,等待JS引擎線程的執行
  • 固然,該線程是與GUI渲染引擎線程互斥的,當JS引擎執行JavaScript代碼時間過長時會形成頁面的阻塞,也就是爲何咱們要把script標籤在body的最後面引入

三、定時器觸發線程

  • 負責執行定時器一類函數的進程,如settimeout、setInterval
  • 當主線程依次執行代碼時,遇到計時器,會將計時器交給該線程處理,當計時完畢以後,定時器觸發線程會將計時完畢後的事件加入到事件隊列的尾部,等待JS引擎線程的執行

四、事件觸發線程

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

五、異步HTTP請求線程

  • 負責執行異步請求一類的函數,如ajax,fetch,axios等
  • 當主線程依次執行代碼時,遇到異步請求,會將函數交給改線程處理,當監聽狀態碼變動時,若是有回調函數,會將回調函數加入到任務隊列的尾部,等待JS引擎線程的執行

4、任務隊列

單線程就意味着,全部任務的執行都須要排隊,前一個任務結束,後一個任務才能執行,若是一個任務耗時很長,後一個任務不得不一直等待着。JavaScript的做者意識到這個問題,將全部任務劃分爲兩種,一種是同步任務,一種是異步任務同步任務是指在主線程上排隊執行的任務,前一個任務結束,後一個任務才能執行。異步任務是指不進入主線程執行,而進入「任務隊列」的任務,只有當「任務隊列」通知主線程能夠執行了,該任務纔會進入主線程。異步任務分爲兩種,宏任務微任務(後面會重點介紹)。接下來經過兩個例子來講明同步任務和異步任務的主要區別:axios

console.log('a')
while (true) {
    console.log('這裏是while')
}
console.log('b')複製代碼

最後打印的結果是a,由於上述代碼均屬於同步任務,由上到下依次執行,當主線程執行完console.log('a')以後,開始執行while循環出現死循環,無限執行console.log('這裏是while'),致使內存溢出,致使while循環後面的任務就沒法執行了。瀏覽器

console.log('a')
settimeout(function () {
    console.log('settimeout1')
},0)
while (true) {
    console.log('這裏是while')
} 複製代碼

最後的打印結果仍是a,由於這段代碼中同時存在同步任務和異步任務,異步任務要等到主線程上全部的同步任務執行完成以後才能執行。上述代碼中的console.log('a')和while循環均屬於同步任務,而settimeout屬於異步任務(在後面的事件循環中會介紹哪些事件屬於異步任務),因此當執行完console.log('a')以後,主線程將執行while循環,無限執行console.log('這裏是while'),最後致使內存溢出,沒法執行下面的代碼了
多線程

5、事件循環(Event Loop)

下圖爲一個完整的事件循環的過程:dom


事件循環的運行機制:異步

  • 一開始執行棧空,咱們能夠把執行棧認爲是一個存儲函數調用的棧結構,遵循先進後出的原則微任務隊列空,宏任務隊列裏有且只有一個script腳本(總體代碼)。
  • 全局上下文(script 標籤)被推入執行棧,同步代碼執行。在執行的過程當中,會判斷是同步任務仍是異步任務,經過對一些接口的調用,能夠產生新的宏任務與微任務,它們會分別被推入各自的任務隊列裏。同步代碼執行完了,全局script腳本會被移出宏隊列,這個過程本質上是隊列的宏任務的執行和出隊的過程。

  • 上一步咱們出隊的是一個宏任務,這一步咱們處理的是微任務。但須要注意的是:當宏任務出隊時,任務是一個一個執行的;而微任務出隊時,任務是一隊一隊執行的。所以,咱們處理微任務隊列這一步,會逐個執行隊列中的任務並把它出隊,直到隊列被清空。

主線程從「任務隊列」讀取事件這個過程,是循環不斷的,因此整個這種運行機制就叫作Event Loop(事件循環)。每當主線程爲空時,就會去讀取「事件隊列」,這就是JavaScript的運行機制


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

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

  • 宏任務包括:全局script任務、setTimeout、setInterval、setImmediate、I/O操做、UI rendering
  • 微任務包括:new Promise.then()、MutationObserver(HTML5新特性)等

當主線程上的全部同步任務執行完以後,是先執行宏任務仍是先執行微任務呢?

  • 因爲代碼入口都是全局任務script,而全局任務script屬於宏任務,因此當棧爲空或者同步代碼執行完以後,會先執行微任務隊列裏的任務
  • 當微任務隊列裏的全部任務都執行完成以後,主線程會讀取宏任務最前面的任務
  • 執行宏任務的過程當中,遇到微任務,依次加入微任務隊列
  • 當前主線程上的調用棧爲空時,再次讀取微任務隊列的任務,以此類推


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

Promise.resolve().then(() => {
    console.log('Promse1')
    setTimeout(function () {
        console.log('setTimeout1')
    }, 0)
})
setTimeout(function () {
    console.log('setTimeout2')
    Promise.resolve().then(() => {
        console.log('Promise2')
    })
}, 0)複製代碼

最終打印結果依次爲Promise一、setTimeout二、Promise二、setTimeout1

  • 一開始執行棧全部的同步任務執行完成,主線程會去讀取微任務隊列(此時微任務隊列有且只有一個微任務),執行微任務中的任務打印出Promise1,同時也會生成一個宏任務setTimeout1
  • 當執行棧爲空時,主線程又會去讀取宏任務隊列最前面的任務。此時,宏任務隊列依次排列着[setTimeou2, setTimeout1],因此setTimeout2執行打印setTimeout2,同時生成一個微任務Promise2加入微任務隊列
  • 當主線程執行完宏任務setTimeout2以後,調用棧爲空,去讀取微任務隊列,此時,微任務隊列只有一個微任務Promise2,執行微任務中的任務打印Promise2
  • 當主線程執行完微任務Promise2以後,調用棧爲空,去讀取宏任務隊列,此時,宏任務隊列就只剩下setTimeout1了,執行setTimeout1打印setTimeout1。
相關文章
相關標籤/搜索