js異步梳理:1.從瀏覽器的多進程到JS的單線程,理解JS運行機制

你們很早就知道JS是一門單線程的語言。可是也時不時的會看到進程這個詞。首先簡單區分下線程和進程的概念javascript

1. 簡單理解進程

- 進程是一個工廠,工廠有它的獨立資源

- 工廠之間相互獨立

- 線程是工廠中的工人,多個工人協做完成任務

- 工廠內有一個或多個工人

- 工人之間共享空間

2. 簡單理解線程

- 工廠的資源 -> 系統分配的內存(獨立的一塊內存)

- 工廠之間的相互獨立 -> 進程之間相互獨立

- 多個工人協做完成任務 -> 多個線程在進程中協做完成任務

- 工廠內有一個或多個工人 -> 一個進程由一個或多個線程組成

- 工人之間共享空間 -> 同一進程下的各個線程之間共享程序的內存空間(包括代碼段、數據集、堆等)

3. 瀏覽器是多進程的

上面的1.1和1.2可能仍是有些抽象。接下來用與前端息息相關的瀏覽器爲例展開。html

當你打開瀏覽器開了好幾個網頁的時候,打開瀏覽器的任務管理器(好比谷歌瀏覽器-> 更多工具 -> 任務管理器)
這裏就是查看進程的地方,並且能夠看到每一個進程的cpu佔用率和內存資源信息。前端

簡單用比較官方的術語總結下:html5

  • 進程是cpu資源分配的最小單位(是能擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程中能夠有多個線程)
  • 不一樣進程之間也能夠通訊。(好比網頁是一個進程,qq是一個進程,在網頁上使用快捷方式qq登陸。網頁怎麼會知道你當前有沒有登陸qq的?這之間就涉及到了不一樣進程之間的通訊)
  • 通常討論的單線程和多線程,都只是指在一個進程內的單和多。

4 瀏覽器是如何渲染進程的?與JS的單線程有什麼聯繫?

在瀏覽器中打開一個網頁至關於新起了一個進程,每一個進程內又會有本身的多線程(固然,瀏覽器有自身的優化機制,當你開了不少空的標籤頁的時候,可能會發現多個空白標籤頁被合併成了一個進程)。好比頁面的渲染,JS的執行,事件的循環,都會在這個進程內進行。(如下用比較官方的術語列舉一些主要常駐線程)java

擴散思考1:瀏覽器爲何要弄成多進程的?jquery

優勢:ajax

  • 避免單個標籤頁崩潰影響整個瀏覽器
  • 避免第三方插件崩潰影響整個瀏覽器
  • 多進程充分利用多核優點
  • 方便使用沙盒模型隔離插件等進程,提升瀏覽器穩定性

缺點:segmentfault

  • 會佔用更多的內存

4.1. GUI渲染線程

  • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和RenderObject樹,佈局和繪製等。
  • 當界面須要重繪(Repaint)或因爲某種操做引起迴流(reflow)時,該線程就會執行(擴展閱讀:頁面重繪和迴流以及優化
  • 注意:GUI渲染線程和JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起(想當與被凍結了),GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
    <style>
        .a {
            width: 100px;
            height: 100px;
            background: #f60;
        }
    </style>    
    <script>
            console.time('js執行')
            for(var i = 0; i < 1000000000; i++) {
                
            }
            console.timeEnd('js執行')
        
    </script>
</head>
<body>
    <div class="a">a</div>
</body>
</html>

從這個例子中能夠看到JS頁面明顯有一段空白期,也就證實了上面所說的當JS引擎執行時GUI線程會被掛起。promise

擴展思考:你可能之前據說而且一直是這麼作的,JS調用不放在中,要放到網頁底部前面來優化你的網站。可是修改這個例子可能會發現不管你是將這段script包含的代碼放到head裏仍是body裏,或者是另外新建一個文件引入,都要等到js加載而且執行纔會在頁面裏渲染出a。尤爲是jquery時代你們統一會將代碼寫在$(document).ready中,那樣的話JS無論在頂部引入仍是在底部引入,這樣看起來它們的執行時機對頁面的影響是同樣的,那JS調用放在頂部和底部真的會有差異嗎?瀏覽器

推薦閱讀:網站爲何 JS 調用盡可能放到網頁底部?

4.2. JS引擎線程

  • 也稱爲JS內核,負責處理Javascript腳本程序。(例如V8引擎)。
  • JS引擎線程負責解析Javascript腳本,運行代碼
  • JS引擎椅子和等待着任務隊列中任務的到來,而後加以處理,一個標籤頁中不管何時都只有一個JS線程在運行JS程序
  • 一樣注意,GUI渲染線程和JS引擎線程是互斥的,因此若是JS執行的時間過長,這樣就會形成頁面的渲染不連貫,致使頁面渲染加載阻塞。

4.3. 事件觸發線程

  • 歸屬於瀏覽器而不是JS引擎,用來控制事件循環
  • 當JS引擎執行代碼塊如setTimeOut時(也可來自瀏覽器內核的其它線程,如鼠標點擊、Ajax異步請求等),會將對應任務添加到事件線程中
  • 當對應的事件符合觸發條件被出發時,該線程會把事件添加到待處理隊列的末尾,等待JS引擎的處理
  • 注意,因爲JS的單線程的關係,因此這些處理隊列中的事件都得排隊等待JS引擎處理(當JS引擎空閒時纔會去執行)

4.4 定時觸發器線程

  • setInterval 與 setTimeout所在的線程
  • 瀏覽器定時計數器並非由JavasScript引擎計數的。(由於JavaScript引擎是單線程的,若是處於阻塞線程狀態就會影響計時的準確)
  • 所以經過單獨線程來計時並觸發定時(計時完畢後,添加到事件隊列中,等待JS引擎空閒後執行)
  • 注意,W3C在HTML表中中規定,規定要求setTimeout低於4ms的時間間隔算爲4ms。

4.5 異步http請求線程

  • 在XMLHttpRequest在鏈接後是經過瀏覽器新開一個線程請求
  • 將檢測到狀態變動時,若是設置有回調函數,異步線程就產生狀態變動事件,將這個回調再放入事件隊列中。再由JavaScript引擎執行。

看了上面的描述後,思考兩個問題。

a. 平時前端寫的事件,定時器,異步咱們都會把它稱爲JS。那爲何這裏把JS引擎線程單獨拿出來說?咱們平時說的JS和這裏的JS引擎有什麼區別?

JS引擎包含兩個部分
內存堆(Memory Heap): 和內存分配有關。(好比基本類型值存棧內存裏,引用類型值存堆內存裏)
調用棧(Call Stack): 代碼執行時候的棧幀 (你可能看到過一些執行棧,執行上下文堆棧,函數調用棧這樣的詞,其實不必太過咬文嚼字。簡單理解就是每當一個函數被調用的時候,都會爲這個函數建立一個新的上下文。而在一個javascript程序中,一定會有多個執行上下文。javascript以棧(先進後出,後進先出)的方式來處理它們。而調用棧就像一個高速攝影機,會把當前運行的代碼的每一幀都給記錄下來。)

推薦閱讀:js基礎梳理-究竟什麼是執行上下文棧(執行棧),執行上下文(可執行代碼)?

而平常開發中真實的JS運行環境可能包含更多的內容,好比DOM操做(onload, onclick...), Ajax, setTimeout等等。這些是宿主環境(瀏覽器)提供的Web API。而WebAPI自己是不能把執行代碼放到調用棧中執行的,每一個Web Api在執行完成之後會把回調放到事件隊列中。而Event Loop(事件輪詢機制)就是檢查執行棧和任務隊列,若是執行棧已經爲空了,就會將事件隊列中的第一個回調函數放到棧中執行。

事件輪詢機制代碼演示

b. 單從前端開發來說,除了上面說的dom操做,定時器,ajax。還有哪些你以爲是異步操做的?

promise, Generator, async

Vue.nextTick的原理和用途

爲何setState是異步的

看一段代碼:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
    <script>
        setTimeout(function() {
            console.log(1);
        })

        new Promise(function(resolve) {
            console.log(2);
            for(var i = 0; i < 1000; i++) {
                i == 99 && resolve();
            }
            console.log(3);
        }).then(function() {
            console.log(4);
        })

        console.log(5);
    </script>
</body>
</html>

打印順序是 2 3 5 4 1。這裏的promise它的執行順序又是怎麼定的

這裏扯出來另外一個概念,宏任務和微任務。前面事件輪詢機制(Event-Loop)中說到任務隊列,一些Web Api 產生的回調函數在條件達到的時候會被加到任務隊列中。而任務隊列又分爲宏任務(macro-task)和微任務(micro-task)。最新的標準中,它們分別被稱爲task 和jobs。

  • 常見的macro-task大概包括:script(總體代碼), setTimeout,setInterval,setImmediate, I/O, UI rendering

  • 常見的micro-task大概包括:process.nextTick, Promise, MutationObserver(html5新特性)

  • setTimeout/promise這些咱們都稱之爲任務源,而進入任務隊列的是它們指定的具體執行任務。好比setTimeout的第一個參數回調函數纔是進入任務隊列的任務。

  • 不一樣任務源的任務會進入到不一樣的任務隊列,其中,setTimeout和setInterval是同源的

  • 事件循環的順序,決定了Javascript代碼的執行順序。它從script(總體代碼)開始第一次循環。而後全局上下文進入函數調用棧。直到調用棧清空(只剩全局),而後執行全部的微任務。當全部可執行的微任務執行完畢以後。循環再去從宏任務去找看看還有沒有其它的宏任務隊列,若是有的話就開始第二輪。

上面這段代碼的執行順序就是:

  1. 事件循環從宏任務隊列開始,宏任務隊列中只有一個script(總體代碼)任務。全局上下文入棧
  2. script宏任務執行時,首先遇到了 setTimeout, 就會在宏任務中添加一個setTimout隊列。
  3. script執行時遇到Promise實例。Promise構造函數的第一個參數,是在new的時候執行,不會進入到任何其它的隊列。而是直接在當前任務直接執行了。因此先打印2
  4. 再往下for循環也不會進入其它隊列,因此繼續打印2
  5. 接下來到then了。promise的 .then 會被分發到 微任務的 Promise隊列中去
  6. script繼續往下執行。打印5。到此,全局任務就執行完了。
  7. 第一個宏任務script執行完了以後,就開始執行全部的可執行的微任務。這時候,微任務中,只有一個promise隊列的任務console.log(4)。就打印了4
  8. 當全部的微任務執行完了以後,表示第一輪的循環就結束了。這時候繼續第二輪的循環。第二輪的循環依然從宏任務開始,它就找到了 setTimout隊列中還要一個 console.log(1) 的任務要執行。因此就打印了1。這時候發現宏任務隊列和微任務隊列中都沒有任務了,因此代碼就不會再輸出其它東西了。
相關文章
相關標籤/搜索