瀏覽器與JS運行機制

1、JavaScript預解析

JavaScript代碼運行分爲兩個階段:javascript

  • (1) 預解析

全部函數定義提早,函數體提高(固然不包括如var box = function() {} )
形參聲明並賦值
變量聲明(不賦值)前端

  • (2) 執行

按照js運行機制從,從上到下執行java

2、進程與線程

  • 進程是cpu資源分配的最小單位(是可以擁有資源和獨立運行的最小單位)
  • 線程是cpu調度的最小單位(線程是創建在進程的基礎上的一次程序運行單位,一個進程能夠有多個線程

舉例:此處有多個工廠,每一個工廠有1個或多個工人。此時工廠就比如進程,有單獨專屬本身的工廠資源;工人就比如是線程,多個工人在工廠中寫做工做。工廠的空間是工人們共享的,這象徵一個進程的內存空間是共享的,每一個線程均可以共享內存。而且每一個工廠之間相互獨立存在。
進程和線程.pngnode

  • 應用程序必須運行在某個進程的某個線程上
  • 一個進程至少有一個運行的線程:主線程,進程啓動後自動建立

3、瀏覽器進程

瀏覽器內核是指支持瀏覽器運行的最核心的部分,分爲渲染引擎和JS引擎。如今JS引擎比較獨立,內核更加傾向於說渲染引擎ios

(1)瀏覽器內核分類git

  • Chrome、Safari: Webkit (Bink)
  • Firefox:Gecko
  • IE:Trident
  • 360、搜狗等國內瀏覽器:Trident+Webkit
  • ...

(2)瀏覽器進程github

  • 瀏覽器是多進程的
  • 瀏覽器之因此能運行,是由於系統給它的進程分配了資源(cpu、內存)
  • 簡單來講,每打一個Tab頁,就至關於建立了一個獨立的瀏覽器進程

瀏覽器進程的組成:ajax

  • Browser進程

瀏覽器的主進程,負責協調、主控,只有一個。
負責內容:瀏覽器頁面顯示;與用戶交互(前進、後退等);網絡資源的管理、下載;各個頁面的管理,建立和銷燬其餘進程等axios

  • 第三方插件進程
    每種類型的插件對應一個進程,僅當插件使用時才建立
  • GPU進程
    最多一個,用於3D繪製等
  • 瀏覽器渲染進程(瀏覽器內核,Renderer進程,內部是多線程的)
    默認 每一個Tab頁面一個進程,互不影響
    負責內容:頁面渲染;腳本執行;事件處理

瀏覽器是多線程的優點:避免單個Tab頁崩潰或單個插件崩潰影響其餘整個瀏覽器,能夠充分多核優點,方便使用沙盒模型隔離插件等進程,提升瀏覽器的穩定性。缺點是,內存和cpu消耗會更大,有點空間換時間的意思。segmentfault

Borwser進程與瀏覽器內核(Renderer進程)的通訊過程:

  • Browser進程收到用戶請求,首先須要獲取頁面內容(譬如經過網絡下載資源),隨後將該任務經過RendererHost接口傳遞給Render進程

    • 渲染線程接收請求,加載網頁並渲染網頁,這其中可能須要Browser進程獲取資源和GPU進程來幫助渲染
    • 固然可能會有JS線程操做DOM(可能會形成迴流並重繪)
    • 最後Renderer進程將結果傳遞給Browser進程
  • Renderer進程的Renderer接口收到消息,簡單解釋後,交給渲染線程,而後開始渲染
  • Browser進程收到結果並將結果繪製出來

4、瀏覽器渲染進程

對於前端操做來講 ,最重要的是渲染進程,而且渲染進程也是多線程的
渲染進程包含哪些線程?

  • GUI渲染線程

    • 負責渲染瀏覽器頁面,解析HTML、CSS,構建DOM樹和RenderObject樹,佈局和繪製等
    • 負責重繪(Repaint)和迴流(Reflow)
    • GUI渲染線程和JS引擎線程是互斥的,當JS引擎執行時GUI線程會被掛起,GUI線程會保存在一個隊列裏等 js引擎空閒時執行。
  • JS引擎線程

    • 負責處理JavaScript腳本,執行代碼
  • 事件觸發線程

    • 主要負責將準備好的事件交給JS引擎線程執行

好比setTimeout定時器計數結束、ajax等異步請求成功並觸發回調函數、用戶觸發點擊事件等,該線程會將整裝待發的事件加入到任務隊列的隊尾,等待JS引擎線程的執行。

  • 定時器觸發線程

    • 主要負責異步定時器一類的函數處理,如setTimeout、setInterval

主線程依次執行代碼時,遇到定時器,會將定時器交給該線程處理。當計數完畢後,事件觸發線程會將計數完畢的事件加入到任務隊列的尾部,等待JS引擎線程執行。

  • 異步HTTP請求線程

    • 負責執行異步請求一類的函數,如:ajax、axios、promise等

主線程依次執行代碼是,遇到異步請求,會將異步請求函數交給該線程處理。當監聽到狀態碼變動,若是有回調函數,事件觸發線程會將回調函數加入到任務隊列的尾部,等待 JS引擎線程執行。

5、事件循環

1 瀏覽器中的事件循環

JavaScript語言是單線程的,意思是同一時間只能作一件事。後來爲了有效利用多核CPU的計算能力,HTML5提出Web Server標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,而且子線程不能操做DOM。因此新標準並無改變JavaScript單線程的本質。

簡單描述JS的執行機制:

  1. 首先判斷JS是同步任務仍是異步任務,同步任務就進入主線程執行,異步任務進入event table
  2. 異步任務在event table中註冊函數,異步函數又分爲宏任務(macro-task)和微任務(micro-task),當知足觸發條件後,宏任務被推入宏任務隊列(macro-task queue),微任務被推入微任務隊列(micro-task queue)
  3. 同步任務在主線程中一直執行,直到同步任務執行完畢,主線程空閒空閒時,纔去微任務隊列(micro-task queue)中查看是否有可執行的異步任務,若是有就推入主線程中執行
  4. 直到所有微任務依次執行完畢後,主線程空閒,再去宏任務隊列(macro-task queue) 查看是否有可執行的異步任務,若是有就推入主線程中執行

以上四步循環執行,就是event loop。

一個完整的Event Loop過程:

① 全部的同步任務都在主線程上執行,造成一個執行棧(exection context stack),咱們能夠認爲執行棧是一個函數調用的棧結構,遵循先進後出的原則。除了主線程的執行棧,還存在一個任務隊列(task queue),任務隊列分爲宏任務隊列(macro-task queue)和微任務隊列(micro-task queue)。
一開始執行棧爲空,宏任務隊列(macro-task queue)裏只有一個script代碼(總體代碼),微任務隊列(micro-task queue)隊列爲空。
② 宏任務隊列(macro-task queue)中的全局上下文(script標籤)會被推入執行棧,同步代碼執行。在執行的過程當中會判斷是同步任務仍是異步任務,同步任務依次執行,異步任務會經過對一些接口的調用而產生新的macro-task和micro-task(只要異步任務有了運行結果,就會在對應的任務隊列中放置一個事件,等待調用)。同步代碼執行完了,script腳本會行和出隊的過程。
③ 上一步出隊的是一個macro-task,這一步要處理的是micro-task。須要注意的是,當macro-task出隊時,任務是一個一個執行的,而micro-task出隊時,任務是一隊一隊執行的。所以,咱們處理micro-task這一步,會逐個執行隊列中的任務並把它出隊,直到隊列被清空。
④ 執行渲染操做,更新頁面
⑤ 檢查是否存在Web worker任務,若是有,則對其進行進行處理
⑥ 上述過程重複循環,直到兩個隊列都清空
event-loop.jpg

宏任務隊列能夠有多個,而微任務隊列只有一個:

  • 常見的macro-task:setTimeout、setInterval、script(整套代碼)、I/O操做、UI渲染等;
  • 常見的micro-task:new Promise().then(回調)、process.nextTick、MutationObserver(HTML5新特性)等

2 Node中的事件循環

Node中的事件循環與瀏覽器的是徹底不一樣不一樣的東西。Node採用V8做爲js的解析引擎,而I/O處理方面使用本身設計的libuv。
libuv是一個基於事件驅動的跨平臺抽象層,封裝了不一樣操做系統的一些底層特性,對外提供API,事件循環也是在它裏面實現:
node-eventLoop.png

NodeJS運行機制以下:

  • V8引擎解析JavaScript腳本
  • 解析後的代碼調用Node API
  • libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎
  • V8引擎再將結果返回給用戶

libuv引擎的事件循環分爲6個階段:

  1. timers階段:執行timers(setTimeout和setInterval)的回調
  2. I/O callbacks階段:處理上一輪循環少數未執行的的I/O回調
  3. idel、prepare階段:僅Node內部使用
  4. poll階段:獲取新的I/O事件,執行I/O回調
  5. check階段:執行setImmediate()回調
  6. close callbacks階段:執行socket的close事件回調

絕大部分的異步任務都在timers、poll、check這個3個階段處理

NodeJS執行環境下的特殊狀況:
1)setTimeout和setImmediate
兩者很是類似,區別主要在於調用時機不一樣:

  • setImmediate設計在poll階段完成時執行,即check階段
  • setTimeout設計在poll階段爲空閒時,且設定階段到達後執行,但它在timers階段執行
setTimeout(function timeout () {
  console.log('timeout');
},0);

setImmediate(function immediate () {
  console.log('immediate');
});

對於以上代碼,setTimeout可能執行在前,也可能執行在後;
取決於setImmediate的準備時間;由於當setTimeout指定時間小於4ms,則增長到4ms(4ms是H5de新標準,2010年之前的瀏覽器是10ms)

可是若是兩者在I/O callback內部回調時,老是先執行setImmediate,後執行setTimeout:

const fs = require('fs')
fs.readFile(__filename, () => {
    setTimeout(() => {
        console.log('timeout');
    }, 0)
    setImmediate(() => {
        console.log('immediate')
    })
})
// immediate
// timeout
// 由於這兩個代碼都寫在I/O回調中,I/O回調是在poll階段執行,當回調執行完畢後隊列清空,發現SetImmediate回調,因此當即跳轉到check階段執行回調。
});

2)process.nextTick

process.nextTick是獨立於Event Loop以外的,它有一個本身的隊列,會優先於其餘micro-task隊列執行:

setTimeout(() => {
    console.log('timer1')
    Promise.resolve().then(function() {
          console.log('promise1')
    })
}, 0)

process.nextTick(() => {
    console.log('nextTick')
    process.nextTick(() => {
        console.log('nextTick')
        process.nextTick(() => {
            console.log('nextTick')
            process.nextTick(() => {
                console.log('nextTick')
            })
           })
     })
})
// nextTick=>nextTick=>nextTick=>nextTick=>timer1=>promise1

3 瀏覽器與Node的Event Loop差別

瀏覽器環境下,micro-task的任務隊列是每一個macro-task執行以後執行;
Node環境下,在node10及其之前版本,micro-task會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會執行micro-task隊列的任務
Node在node11版本開始,Event Loop的運行原來發生了變化,一旦一個階段裏的宏任務執行完,就會當即執行微任務隊列,這一點與瀏覽器一直。

diff-eventLoop.png

4 Web worker

因爲JS是單線程,當遇到計算密集型或高延遲的任務,用戶界面可能會短暫「凍結」,不能作其餘操做。
因而HTML5提出Web Worker,它容許JavaScript創造多線程環境,容許主線程建立Worker線程,將一些任務分配給後者。主線程運行的同時,Worker線程在後臺運行,二者互不干擾,等到Worker完成計算任務,在把結果返回給主線程。
Web Worker的優勢是能夠承擔一些密集型或高延遲任務,使主線程流暢,不被阻塞或拖慢。
缺點:

  • 不能跨域加載JS
  • Worker內部代碼不能訪問DOM
  • 不是全部瀏覽器都支持這個新特性

Web Worker使用方法:

  • 主線程調用Worker線程:

    1. 主線程經過new Worker()調用Worker構造函數,新建一個Worker線程
    2. 主線程調用worker.postMessage()方法,向Worker發消息
    3. 主線程經過worker.onmessage指定監聽函數,接收子線程發回來的消息
// 主線程:
var input = document.getElementById('number')
document.getElementById('btn').onclick = function () {
    var number = input.value
    //一、建立一個Worker對象
    var worker = new Worker('worker.js')
    // 三、綁定接收消息的監聽
    worker.onmessage = function (event) {
        console.log('主線程接收分線程返回的數據: '+event.data)
        alert(event.data)
    }
    // 二、向分線程發送消息
    worker.postMessage(number)
    console.log('主線程向分線程發送數據: '+number)
}
console.log(this) // window

Worker線程響應:

  1. Worker內部經過onmseeage()監聽事件
  2. 經過postMessage(data)方法向主線程發送數據
//worker.js文件
function fibonacci(n) {
    return n<=2 ? 1 : fibonacci(n-1) + fibonacci(n-2)  //遞歸調用
}
console.log(this)//[object DedicatedWorkerGlobalScope]
this.onmessage = function (event) {
    var number = event.data
    console.log('分線程接收到主線程發送的數據: '+number)
    //計算
    var result = fibonacci(number)
    postMessage(result)
    console.log('分線程向主線程返回數據: '+result)
    // alert(result)  alert是window的方法, 在分線程不能調用
    // 分線程中的全局對象再也不是window, 因此在分線程中不可能更新界面
}
參考資料:
https://github.com/ljianshu/B...
https://juejin.im/post/5bb054...
深刻淺出JavaScript運行機制
10分鐘理解JS引擎的執行機制
瀏覽器組成
全面梳理JS引擎的運行機制
相關文章
相關標籤/搜索