JavaScript的運行機制

1、爲何JavaScript是單線程?

  JavaScript的特色就是單線程,也就是說同一時間只能作一件事情,前面的任務沒作完,後面的任務只能處於等待狀態,(這就跟生活中的例子:排隊買票同樣,一個一個排隊按順序來)。這就產生了一個問題:爲何JavaScript不能是多線程的呢?多線程能夠提升多核CPU的利用率,從而提升計算能力啊,這與瀏覽器的用途是息息相關的,也能夠說是瀏覽器的用途直接決定了JavaScript只能是單線程。前端

  假如說JavaScript是多線程,咱們能夠試想一下,若是某一時刻一個線程給某個DOM節點添加內容,另外一個線程在刪除這個DOM節點,這個時候瀏覽器聽那一個線程的?這樣會讓程序變得很是複雜,並且徹底沒有必要。同時這也說明JavaScript的單線程與它的用途有關,瀏覽器的主要用途是操做DOM、與用戶完成互動、添加一些交互行爲,若是是多線程將會產生很是複雜的問題。因此JS從一開始起爲了不復雜的問題產生,JavaScript就是單線程的,單線程已經成爲JavaScript的核心,並且從此也不會改變。node

  在多核CPU出現以來,這給單線程帶來了很是的不便,不可以充分發揮CPU的做用。爲了提升現代多核CPU的計算能力,HTML5提出了Web Worker標準,容許JavaScript腳本建立多個線程,可是,這個本質並無改變JavaScript單線程的本質。ios

  什麼是單線程與多線程,這個問題值得咱們思考?ajax

    單線程:一個進程中只有一個線程在執行,若是把進程比做一間工廠,線程比做一個工人,所謂單線程就是這個工廠中只有一個工人在工做數據庫

    多線程:一個進程中同時有多個線程在執行,比如這個工廠中有多個工人在一塊兒協同工做npm

    進程:CPU資源分配的最小單位,一個程序要想執行就須要CPU給這個程序分配相應的資源出來,用完以後再收回去。例如:內存的佔用,CPU給這個程序提供相應的計算能力。axios

    線程:CPU調度的最小單位,一個程序能夠理解爲有N個任務組成的集合,某一時刻執行那個任務就須要線程的調度promise

    圖解:瀏覽器

    

    注意點:有圖可知:工廠空間是共享的,說明一個進程中能夠存在一個或多個線程,工廠資源是共享的,說明一個進程的內存空間能夠被該進程中全部線程共享,多個進程之間相互獨立,例如:聽音樂的時候,不會影響到敲代碼,歌詞是不會出如今代碼編輯器中的。服務器

      多進程:在同一時間裏,同一臺計算機系統中容許兩個或兩個以上的進程處於運行狀態,其實如今的計算機開機狀態下就是多個進程在運行,打開任務管理器就能夠看到那些進程在運行。多進程帶來的好處是很是明顯的,能夠充分利用CPU的資源,並且電腦同時能夠幹不少件事還互相不影響。例如:在使用編輯器寫代碼的時候,還可使用QQ音樂聽歌。編輯器和QQ音樂之間徹底不會影響。以Chrome爲例:每打開一個tab就就至關於開啓了一個進程,每一個tab之間是徹底不會影響。

  上面提到HTML5的Web Worker能夠改善單線程的不便,瞭解Web Worker須要注意如下幾點:

  • 同源限制

    分配給Worker子線程運行的腳本文件,必須與主線程的腳本文件同源。

  • DOM限制

    Worker線程與主線程不同,沒法讀取主線程所在網頁的DOM對象,沒法使用 document 、 window 、 parent 等對象,可是可使用 Navigator 對象、 location 對象

  • 全局對象限制

    Worker 的全局對象 WorkerGlobalScope ,不一樣於網頁的全局對象 Window ,不少接口拿不到,理論上Worker不能使用 console.log 

  • 通訊聯繫

    Worker 線程和主線程不在同一個上下文環境,它們不能直接通訊,必須經過消息完成。

  • 腳本限制

    Worker 線程不能執行alert()方法和confirm()方法,但可使用 XMLHttpRequest 對象發出 AJAX 請求。

  • 文件限制

    Worker 線程沒法讀取本地文件,即不能打開本機的文件系統(file://),它所加載的腳本,必須來自網絡。

2、瀏覽器的渲染流程

  在理解渲染原理以前,先了解瀏覽器內核構成是很是有必要的,具體內容看下面:

    瀏覽器工做方式:瀏覽器內核是經過取得頁面內容、整理信息(應用CSS)、計算和組合最終輸出可視化的圖像結果。

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

    1. GUI渲染線程
    2. JavaScript引擎線程
    3. 定時觸發器線程
    4. 事件觸發線程
    5. 異步Http請求線程

    GUI渲染線程:

    • 主要負責頁面的渲染,解析HTML、CSS,構建DOM樹,佈局和繪製等。
    • 當界面須要重繪或者因爲某種操做引起迴流時,將執行該線程。
    • 該線程與JS引擎線程互斥,當執行JS引擎線程時,GUI渲染會被掛起,當任務隊列空閒時,主線程纔會去執行GUI渲染,這也是爲何js解析腳本的時候會阻塞界面的渲染。

    JS引擎線程

    • 該線程固然是主要負責處理 JavaScript腳本,執行代碼。
    • 也是主要負責執行準備好待執行的事件(異步事件),即定時器計數結束,或者異步請求成功並正確返回時,I/O讀取文件等等將依次進入任務隊列,等待 JS引擎線程的執行。
    • 該線程與 GUI渲染線程互斥,當 JS引擎線程執行 JavaScript腳本時間過長,將致使頁面渲染的阻塞。

    定時觸發器線程

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

    事件觸發線程

    • 主要負責將準備好的事件交給 JS引擎線程執行。例如:setTimeout定時器計數結束, ajax等異步請求成功並觸發回調函數,或者用戶觸發點擊事件時,該線程會將相應的事件依次加入到任務隊列的隊尾,等待 JS引擎線程的執行。

    異步http請求線程

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

3、任務隊列

  以前初步瞭解了瀏覽器的內核構成,提到了不少異步事件,那異步事件如何執行呢?這就跟任務隊列有關了。如今來談談什麼是任務隊列,爲何須要任務隊列?

  單線程也就意味着全部任務都須要排隊,只有在前一個任務結束以後,纔會執行後一個任務,不然後面的任務只能處於等待狀態。若是某個任務執行須要很長的時間,後面的任務就都須要等待着,這會形成很是大的交互影響,給用戶有一種頁面加載卡頓的現象,影響用戶體驗!

  若是等待是由於CPU忙不過來也能夠理解,大多數狀況並非CPU忙不過來的緣由,而是文件I/O的讀取,網絡請求、鼠標點擊事件等這些操做須要花費很是長的時間,只有等這些操做返回結果以後才能往下執行。

  爲了解決這個問題,JavaScript的開發者也很快的意識到,腳本文件的執行,徹底能夠不用管那些很是耗時的I/O設備,異步請求,徹底能夠掛起等待中的任務,執行排在後面的位置,等I/O操做返回結果以後,再回過頭執行掛起的任務。

  根據上面的理解能夠將任務分爲兩種:同步任務(synchronize task)、異步任務(asynchronize task)。

    • 同步任務:在主線程(JS引擎線程)進行排隊的任務,只有在前一個任務結束以後,纔開始執行後一個任務。
    • 異步任務:不進入主線程,進入任務隊列(task queue)的任務,只有在任務隊列通知主線程任務隊列中的某個任務能夠進入主線程了,該任務纔會進入主線程執行,也能夠將異步任務理解爲:有註冊函數和回調兩部分組成,註冊函數負責發起異步過程,回調函數用來負責處理的結果。

  瀏覽器的運行機制

    • 全部同步任務都在主線程上執行,造成一個執行棧(execution context stack)
    • 主線程以外,還存在一個"任務隊列"(task queue)。只要異步任務有了運行結果,就在"任務隊列"之中放置一個事件。
    • 一旦"執行棧"中的全部同步任務執行完畢,系統就會去"任務隊列"找找看,看看裏面有哪些事件,那些對應的異步任務,已經結束等待狀態,獲得的相應的結果,獲得結果的任務結束等待狀態當即進入執行棧,開始執行。
    • 主線程不斷重複上面的第三步的操做,也就造成了一個事件環(Event Loop)

4、事件和回調函數

  任務隊列是一個事件隊列(也能夠理解爲:消息隊列),異步操做如:I/O設備完成一項任務,就在任務隊列中添加一個事件,表示相關的異步任務能夠進入"執行棧"了。主線程讀取"任務隊列",就是讀取裏面的事件。

  任務隊列中的事件,除了IO設備的事件之外,還包括一些用戶產生的事件(好比鼠標點擊、頁面滾動等等)。只要指定過回調函數,這些事件發生時就會進入"任務隊列,等待主線程讀取。

  所謂"回調函數"(callback),就是那些會被主線程掛起來的實現該任務的代碼,主線程幹開始運行的時候是不會執行的。異步任務必須指定回調函數,當主線程開始執行異步任務,就是執行對應的回調函數代碼。

  任務隊列是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。主線程的讀取過程基本上是自動的,只要執行棧一清空,"任務隊列"上第一位的事件就自動進入主線程。可是,因爲存在後文提到的"定時器功能「」,主線程首先要檢查一下執行時間,某些事件只有到了規定的時間,才能返回主線程。

5、瀏覽器中的事件循環(Event Loop)

  主線程從"任務隊列"中讀取事件,這個過程是循環不斷的,因此整個的這種運行機制又稱爲Event Loop(事件循環)

  爲了更好的理解事件循環,請看下面的圖:

        

   執行棧中的代碼(同步任務),老是在讀取"任務隊列"(異步任務)以前執行。

   Micro Task 和  Macro Task: 

     瀏覽器端事件循環的異步隊列有兩種:macro(宏任務)隊列、micro(微任務)隊列,macro隊列能夠有多個,micro隊列只能有一個。

     常見的Macro-Task: setTimeout、setInterval、script、I/O操做、UI渲染

     常見的Micro-Task: new Promise.then()  MutationObserve

   如今再拿以前的圖來解釋任務執行的流程,進一步加深理解:

    圖解:

      

    執行流程:

    1. 一開始執行棧爲空,執行棧能夠理解爲「先進後出」的棧結構,micro任務隊列爲空,macro任務隊列中只有一個script 腳本,總體的js代碼。
    2. 全局上下文(script總體代碼)被推入執行棧,代碼開始執行,判斷是同步任務仍是異步任務,若是是異步任務經過對一些接口的調用能夠產生新的macro任務和micro任務,同步代碼執行完畢,script腳本被移除macro任務隊列,這個過程能夠理解爲macro-task的執行和出隊。
    3. 上一步咱們出隊的是一個 macro-task,這一步咱們處理的是 micro-task。 但須要注意的是:當 macro-task 出隊時,任務是 一個一個執行的;而 micro-task 出隊時,任務是 一隊一隊執行的。所以,咱們處理 micro 隊列這一步,會逐個執行隊列中的任務並把它出隊,直到隊列被清空。
    4. 執行渲染操做,更新界面。
    5. 檢查是否存在 Web worker 任務,若是有,則對其進行處理。
    6. 上述過程循環往復,直到兩個隊列都清空。

   總結一下:當某個宏任務執行完後,會查看是否有微任務隊列。若是有,先執行微任務隊列中的全部任務,若是沒有,會讀取宏任務隊列中排在最前的任務,執行宏任務的過程當中,遇到微任務,依次加入微任務隊列。棧空後,再次讀取微任務隊列裏的任務,依次類推。

    代碼演示:

 1 <script>
 2       Promise.resolve().then(() => { // Promise.then()是屬於micro-task
 3         console.log('micro-task1');
 4         setTimeout(() => { // setTimeout是屬於macro-task
 5           console.log('macro-task1');
 6         }, 0)
 7       })
 8       setTimeout(() => {
 9         console.log('macro-task2');
10         Promise.resolve().then(() => {
11           console.log('micro-task2');
12         })
13         console.log('macro-task3');
14       })
15       console.log('同步任務');
16 </script>

 

   運行結果爲:同步任務--->micro-task1 --->macro-task2--->macro-task3--->micro-task2--->macro-task1

     1.代碼開始執行,判斷是同步任務仍是異步任務,檢測到有同步任務(屬於宏任務)存在,先輸出「同步任務」

     2. 同步任務執行完去查看是否有微任務隊列存在,上面代碼的微任務隊列爲:promise.resolve().then(),開始執行微任務,輸出micro-task1

     3.執行微任務的過程當中發現有宏任務setTimeout()存在,將其添加到宏任務隊列,微任務執行完畢開始執行宏任務,因爲macro-task2所在的宏任務早於macro-task1,所以先執行macro-task2所在的宏任務,輸出macro-task2

     4.輸出macro-task2以後發現存在微任務micro-task2,將其添加到微任務隊列,接着輸出macro-task3

     5.宏任務執行完畢,接着開始執行微任務輸出:micro-task2

     6.微任務執行完畢,接着執行macro-task1所在的宏任務,輸出:macro-task1

     7.執行完畢宏任務,此時的macro-task隊列和micro-task隊列已空,程序中止。

6、Node.js中的事件循環

  Node.js 不是一門語言也不是框架,它只是基於 Google V8 引擎的 JavaScript 運行時環境,同時結合 Libuv 擴展了 JavaScript 功能,使之支持 io、fs 等只有語言纔有的特性,使得 JavaScript 可以同時具備 DOM 操做(瀏覽器)和 I/O、文件讀寫、操做數據庫(服務器端)等能力,是目前最簡單的全棧式語言。

  目前Node.js在大部分領域都佔有一席之地,尤爲是 I/O 密集型的,好比 Web 開發,微服務,前端構建等。很多大型網站都是使用 Node.js 做爲後臺開發語言的,用的最多的就是使用Node.js作前端渲染和架構優化,好比 淘寶 雙11、去哪兒網的 PC 端核心業務等。另外,有很多知名的前端庫也是使用 Node.js 開發的,好比,Webpack 是一個強大的打包器,React/Vue 是成熟的前端組件化框架。

  Node.js一般被用來開發低延遲的網絡應用,也就是那些須要在服務器端環境和前端實時收集和交換數據的應用(API、即時聊天、微服務)。阿里巴巴、騰訊、Qunar、百度、PayPal、道瓊斯、沃爾瑪和 LinkedIn 都採用了 Node.js 框架搭建應用。

  Node.js 編寫的包管理器 npm 已成爲開源包管理了領域最好的生態,直接到2017年10月份,有模塊超過47萬,每週下載量超過32億次,每月有超過700萬開發者使用npm。

  固然了,Node.js 也有一些缺點。Node.js 常常被人們吐槽的一點就是:回調太多難於控制(俗稱回調地獄)。可是,目前異步流程技術已經取得了很是不錯的進步,從Callback、Promise 到 Async函數,能夠輕鬆的知足全部開發需求。

  至於其餘的特性這裏附一篇很值得一看的文檔:https://cnodejs.org/topic/5ab3166be7b166bb7b9eccf7

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

  

  Node.js的運行機制以下:

    1.V8引擎解析JavaScript腳本

    2.解析後的代碼,調用Node API

    3.libuv庫負責Node API的執行。它將不一樣的任務分配給不一樣的線程,造成一個Event Loop(事件循環),以異步的方式將任務的執行結果返回給V8引擎

    4.V8引擎再將結果返回給用戶

  EventLoop的六個階段

    libuv引擎中的事件循環分爲 6 個階段,它們會按照順序反覆運行。每當進入某一個階段的時候,都會從對應的回調隊列中取出函數去執行。當隊列爲空或者執行的回調函數數量到達系統設定的閾值,就會進入下一階段。

    

    從上面的圖片能夠大體看出node的事件循環的順序爲:外部輸入階段--->輪詢階段(poll)--->檢查階段--->關閉事件回調階段(close callback)--->定時器檢查階段(timer)--->I/O回調階段(I/O callback)--->閒置階段(idle,prepare)--->輪詢階段 ,按照上面的順序循環反覆執行。

    timer階段:這個階段執行setTimeout或setInterval回調,而且是有poll階段控制的。一樣在Node.js中定時器指定的時間也不是很是準確,只能是儘快執行。

    I/O callbacks階段:處理一些上一輪循環中少數未執行的I/O回調

    idle,prepare階段:進node內部使用

    poll階段:獲取新的I/O事件,適當的條件下node將阻塞在這裏

    check階段:setImmediate()回調函數的執行

    close callbacks階段:執行socket的close事件回調。

    注意點:上面的6個階段都是不包括process.nextTick(),在平常的開發中咱們使用最多的就是:timer  poll  check這三個階段,絕大多數的異步操做都是在這三個階段 完成的。

  timer階段:這個階段執行setTimeout或setInterval回調,而且是有poll階段控制的。一樣在Node.js中定時器指定的時間也不是很是準確,只能是儘快執行。

  poll階段:

    1.這個階段是相當重要的階段,系統會作兩件事情。一是回到timer階段執行回調,二是執行I/O回調

    2.若是在進入該階段的時候沒有設置timer,會發生兩件事情:

      2.1.若是poll隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或達到系統限制

      2.2.若是poll階段爲空,會發生下面兩件事:

        2.2.1:若是有setImmediate()回調須要執行,poll階段會中止,進入到check階段執行回調

        2.2.2:若是沒有setImmediate()回調須要執行,會等到回調被加入隊列中並當即執行回調,這裏一樣有個超時限制防止一致等待下去

    3.若是設置了timer且poll隊列爲空,則會判斷是否有timer超時,若是有的話回到timer階段執行回調。

  check階段:setImmediate()的回調會被加入到check隊列中,從event  loop的階段圖能夠看出,check階段的執行是在poll階段以後的。

  microTask和macroTask:

   1.常見的micro-task: process.nextTick()  Promise.then()

   2.常見的macro-task: setTimeout、setIntevaral、setImmeidate、script、I/O操做

   3.先分析一段代碼示例:

console.log('start');
setTimeout(() => {
    console.log('time1');
    Promise.resolve().then(() => {
        console.log('promise1');
    })
}, 0)
setTimeout(() => {
    console.log('time2');
    Promise.resolve().then(() => {
        console.log('promsie2');
    })
}, 0)
Promise.resolve().then(() => {
    console.log('promise3');
})
console.log('end');
// Node中的打印結果:start--->end--->promise3--->time1--->timer2--->promise1--->promise2
// 瀏覽器中的打印結果:start--->end--->promise3--->time1--->promise1--->time2--->promise2

 

    4.node打印結果分析:

      1.先執行同步任務(執行macro-task),打印輸出:start  end

      2.執行micro-task任務:輸出promise3,這一點跟瀏覽器的機制差很少

      3.進入timer階段執行setTimeout(),打印輸入time1,並將promise1放入micro-task隊列中,執行timer2打印time2,而且將promise2放入到micro隊列中,這一點跟瀏覽器的差異比較大,timer階段有幾個setTimeout/setIntever就執行幾個,而不像瀏覽器同樣執行完一個macro-task以後當即執行一個micro-task

   5.setTimeout()和setImmediate()很是類似,區別主要在調用的實際不一樣

      5.1:setTimeout()設置在poll階段爲空閒時且定時時間到後執行,但它們在timer階段執行

      5.2:setImmediate()設置在poll階段完成時執行,即check階段執行

      5.3:實例分析:

1 setImmediate(() => {
2     console.log('setImmediate');
3 })
4 setTimeout(() => {
5     console.log('setTimeout');
6 }, 0)

 

      5.4:上面的代碼執行,返回結果是不肯定的,有可能先執行setTimeout() ,有可能先執行setImmediate(),首先 setTimeout(fn, 0) === setTimeout(fn, 1),這是由源碼決定的 進入事件循環也是須要成本的,若是在準備時候花費了大於 1ms 的時間,那麼在 timer 階段就會直接執行 setTimeout 回調,若是準備時間花費小於 1ms,那麼就是 setImmediate 回調先執行了,能夠把setTimeout的第二個參數設置爲:1 ,2,3,4....看看運行結果

      5.5:當兩者寫在I/O讀取操做的回調中時,老是先執行setImmediate(),由於I/O回調是寫在poll階段,當回調執行完畢後隊列爲空,發現存在 setImmediate 回調,因此就直接跳轉到 check 階段去執行回調了

 1 const fs = require('fs')
 2 fs.readFile(__filename, (err, data) => {
 3     setTimeout(() => {
 4         console.log('setTimeout');
 5     }, 0)
 6     setImmediate(() => {
 7         console.log('setImmediate');
 8     })
 9 })
10 console.log(__filename); // 打印當前文件所在的路徑

   6.process.nextTick():這個函數實際上是獨立於 Event Loop 以外的,它有一個本身的隊列,當每一個階段完成後,若是存在 nextTick 隊列,就會清空隊列中的全部回調函數,而且優先於其餘 microtask 執行。也就是說它指定的任務隊列,都是在全部異步任務以前發生

 1 console.log('start')
 2 setTimeout(() => {
 3     console.log('time1');
 4     Promise.resolve().then(() => {
 5         console.log('promise1');
 6     })
 7 })
 8 Promise.resolve().then(() => {
 9     console.log('promise2');
10 })
11 process.nextTick(() => {
12     console.log('nextTick1');
13     process.nextTick(() => {
14         console.log('nextTick2');
15         process.nextTick(() => {
16             console.log('nextTick3');
17         })
18     })
19 })
20 console.log('end');
21 // 運行結果:start --->end --->nextTick1 --->nextTick2 --->nextTick3 --->promise2 --->time1 --->promise1

7、瀏覽器中Event Loop和Node.js中的差異

  重點:瀏覽器環境下,microtask的任務隊列是每一個macrotask執行完以後執行。而在Node.js中,microtask會在事件循環的各個階段之間執行,也就是一個階段執行完畢,就會去執行microtask隊列的任務。

  圖解:

  

  代碼演示:

console.log('start');
setTimeout(() => {
    console.log('timer1');
    Promise.resolve().then(() => {
        console.log('promise1');
    });
})
setTimeout(() => {
    console.log('timer2');
    Promise.resolve().then(() => {
        console.log('promise2');
    })
}, 0)
console.log('end');
// 瀏覽器模式下輸出:start ---> end ---> timer1 ---> promise1 ---> timer2 ---> promise2
// node環境下面:start ---> end ---> timer1 ---> timer2 ---> promsie1 ---> promise2

 8、總結

 瀏覽器和node環境下、micro-task隊列的執行時機不一樣:

  瀏覽器端,微任務在事件循環的各個階段執行。

  Node端,微任務是在macro-task執行完畢執行。

相關文章
相關標籤/搜索