結合JS運行機制,理解Event Loop

前言

這幾在看Vue的源碼與其相關的博客,看到了關於Vue異步更新策略nextTick的諸多文章,奈何功力不夠深厚,看的是有點矇蔽。主要緣由是這些個模塊,須要對JS的一些運行機制和Event Loop(事件循環)有必定的瞭解,因而決定再一次深刻的去了解這些知識。前端

在後來幾天的學習中,當下就是總結了這幾個矇蔽點(你可先嚐試自我回答一下):面試

  • JS做爲一個單線程語言,是如何實現併發的執行(定時器,http請求)任務的?
  • 什麼是主線程/call stack(執行棧)?
  • 什麼是task queue(任務隊列)?
  • 什麼是(task/macrotask)宏/(microtask)微事件?
  • 所謂的事件循環,在瀏覽器運行層面來講,到底是什麼?
  • 每次事件循環,都幹了什麼事情?

當時在學習的過程當中,我就以下圖這樣的感受:ajax

帶着上面這些個問題,因而開始這幾天的展開式的學習,從Event Loop的概念,在到瀏覽器層面的實際運行原理。瀏覽器

本篇參考諸多文章,借鑑了裏面的不少原話,都在文章的末尾都一一列出了。性能優化

渲染進程

我猜大部分作前端的,都知道Event Loop(事件循環)的概念。可是不少人,對它的瞭解很是的片面。要想知道這個概念到底是什麼,就要瀏覽器是如何運行的提及。markdown

首先,瀏覽器是多進程執行的,可是對於咱們研究最重要的,就是瀏覽器多個進程中的渲染進程,在瀏覽器的運行中,每個頁面都有獨立渲染進程。這個進程分別由以下幾個線程在工做:數據結構

  • GUI渲染線程
  • JS引擎線程
  • 事件觸發線程
  • 定時器觸發線程
  • 異步http請求線程

上面這幾個線程,保證咱們整個頁面(應用)的完整運行。併發

JS引擎線程

JS引擎線程負責解析Javascript腳本,運行代碼,V8引擎就是在這個線程上運行的。異步

  • 這條線程,也就是在事件循環中,我們常說的主線程和call stack(執行棧)。全部的任務(函數),最終都會進入這裏來執行。
  • 只要執行棧空了,它就會不斷的去訪問task queue(任務隊列),一旦任務隊列中有能夠執行的函數,就會壓入棧內執行。
  • 一個Tab頁(renderer進程)中不管何時都只有一個JS線程在運行JS程的,全部JS是單線程的

如今出現了兩個詞:call stack(執行棧),task queue(任務隊列),這裏先來解釋一下,什麼是執行棧。函數

call stack(執行棧)

棧是一個先進後出的數據結構,全部的函數都會逐一的進入到這裏面執行,一旦執行完畢就會退出這個棧。

function fun3() {
        console.log('run');
        throw Error('err');
    }

    function fun2() {
        fun3();
    }

    function fun1() {
        fun2();
    }

    fun1();
複製代碼

這裏我特地在fun3拋出了一個異常,咱們來看一下瀏覽器的輸出:

上面這列出來的一個個函數,就是一個執行棧。這裏我用一個更詳細的圖解來表示一下執行棧的運行過程:

上面這個圖解,是對執行棧運行過程的分佈演示。這個執行棧,就是咱們JS真正運行的地方,函數的調用會在這裏造成一個調用棧,在裏面是一個個執行的,必須得等到棧頂的函數執行完畢退出,才能繼續執行下面的函數。一旦這個棧爲空,它就會去task queue(任務隊列)看有沒有待執行的任務(函數)。

那麼咱們常說的任務隊列,究竟又是一個啥玩意呢?

事件觸發線程

首先,這裏還要強調一下上面的提到的,一個頁面的運行,是須要多個線程配合支持的。

我們常說的任務隊列,就是由這個事件觸發線程來維護的。當時,我看到這個就矇蔽了……尼瑪,JS不是單線程嗎?這條事件觸發線程是怎麼回事?

JS的確是仍是單線程執行的。這個事件觸發線程屬於瀏覽器而不是JS引擎,這是用來控制事件循環,而且管理着一個任務隊列(task queue),然而對自己的JS程序,沒有任何控制權限,最終任務隊列裏的函數,仍是得交回執行棧去執行。

task queue(任務隊列)

那麼這個線程維護的這個task queue到底是幹嗎的呢?

上面在說call stack(執行棧)的時候,我們提到了,一旦執行棧裏面被清空了,它就會來看任務隊列中是否有須要執行的任務(函數)。這個任務隊列可能存放着延期執行的回調函數,相似setTimeout,setInterval(並非說setTimeout和setInterval在這裏面,而是他們的回調函數),還可能存放着Ajax請求結果的回調函數等等。

這裏先看下具體代碼:

console.log('1');

    setTimeout(() => {
        console.log('2');
    }, 1000);

    $.ajax(/*.....*/)
複製代碼

如今咱們來圖解一下,整個運行過程(圖畫的比較醜,別建議):

  • 第一步,console.log方法進入執行棧,執行完畢後退出。

  • 第二步,執行setTimeout方法。你們知道延遲,是須要去讀數的(你能夠理解爲計時),當到了時間,就讓回調進入到任務隊列裏面,去等待執行。然而,這個讀數的工做是誰在作呢?首先確定不是JS引擎線程在作,由於執行棧一次只能執行一個任務,若是在執行棧中去讀數,必然會形成阻塞,因此渲染進程中,有專門的定時器觸發線程來負責讀數,到了時間,就把回調交給任務隊列。

  • 第三步,發起Ajax請求,請求的過程也是在其餘線程並行執行(http請求線程)的,請求有告終果之後,回調函數加入事件觸發線程的任務隊列。

因此,如今應該明白call stack(執行棧),task queue(任務隊列)是怎麼一個工做狀態了吧。這裏說一句不專業的話,可是你能夠這麼去理解:

在瀏覽器環境下的JS程序運行中,其實並非單線程去完成全部任務的,如定時器的讀數,http的請求,都是交給其餘線程去完成,這樣才能保證JS線程不阻塞。

定時器觸發線程

上面咱們提到在執行setTimeoutsetInterval的時候,若是讓JS引擎線程去讀數的話,必然會形成阻塞。這也是不符合實際需求的,因此這件讀數的事情,瀏覽器把它交給了渲染進程中的定時器觸發線程。

一旦,代碼中出現timer類型API,就會交給這個線程去執行,這樣JS引擎線程,就能夠繼續幹別的事情。等到時間一到,這個線程就會將對應的回調,交給事件觸發線程所維護的task queue(任務隊列)並加入其隊尾,一旦執行棧爲空,就會拿出來執行。

可是這裏要提一點,就算執行棧爲空也不必定能立刻執行這個回調,由於task queue(任務隊列)中可能還有不少的待執行函數,因此定時器只能讓它到了時間的加入到task queue中,但不必定可以準時的執行。

異步http請求線程

這個線程就是專門負責http請求工做的。簡單說就是當執行到一個http異步請求時,就把異步請求事件添加到異步請求線程,等收到響應(準確來講應該是http狀態變化),再把回調函數添加到任務隊列,等待js引擎線程來執行。

GUI渲染線程

這個線程要重點說一下。首先這個GUI渲染線程和JS引擎線程是互斥的,說白了就是這兩個同一時間,只能有一個在運行,JS引擎線程會阻塞GUI渲染線程,這也是爲何JS執行時間長了,會致使頁面渲染不連貫的緣由。

  • 負責渲染瀏覽器界面,解析HTML,CSS,構建DOM樹和Rendert樹
  • JS負責操做DOM對象,GUI負責渲染DOM(最耗費性能的地方),GUI線程會在每次循環中,合併全部的UI修改,也是瀏覽器對渲染的性能優化。
  • GUI更新會被保存在一個隊列中等到JS引擎空閒時當即被執行。

小結

經過上面的這些理論,腦海裏應該大體知道瀏覽器層面的JS是如何去工做了的吧。如今應該能夠回答一開上面提出的部分問題了:

  • JS做爲一個單線程語言,是如何實現併發的執行(定時器,http請求)任務的?答案:由於瀏覽器提供了定時器觸發線程和異步Http請求線程,來分擔這些會形成主線程阻塞的工做。
  • 什麼是主線程/call stack(執行棧)?答案:執行棧是JS引擎線程(主線程)中的一個先進後出執行JS程序的地方。一次只容許一個函數在執行,一旦棧被清空,將會輪詢任務隊列,將任務隊列中的函數逐一壓如棧內執行
  • 什麼是task queue(任務隊列)?答案:任務隊列是在事件觸發線程中,一個存放異步事件回調的地方。當定時器任務,異步請求任務在其餘線程執行完畢時,就會將加入隊列的隊尾,而後被執行棧逐一執行。

OK,如今已經解決三個問題,接下來咱們繼續解決剩下的三個問題。這個三個問題,就是從JS事件循環機制的角度來研究了。

Event Loop(事件循環)

我相信大部分搞前端的,都應該知道這玩意。可是,我發現並非每一個人都能說清楚這個東西。完全瞭解這個,對於咱們處理開發中許多異步問題和閱讀源碼,是不少有幫助的。

首先,咱們先開看一張圖(此圖出自於Event Loop的規範和實現):

我以爲若是你看完了上面渲染進程相關知識,在看這個圖,應該是能理解百分之70了吧,剩下百分之是由於裏面出現了microtask queue(微任務隊列)和Promise,mutation observer的相關字眼。

我以爲,在開始瞭解Event Loop以前,有必要提出兩個問題:

  • 爲何要有Event Loop
  • Event Loop的每個循環,幹了些什麼事?

宏/微任務

針對上面給出這個圖出現的一個新詞microtask,來展開進行學習。

首先,先來看一段代碼:

setTimeout(() => {
        console.log(1);
    });

    Promise.resolve().then(() => {
        console.log(2);
    });

    console.log(3);
複製代碼

輸出的結果:3,2,1

在尚未接觸的Event Loop以前,看到這個結果的時候,說實話,我是很懵逼的。

OK,到這裏,咱們須要先知道兩個概念:task(任務),microtask(微任務);

這裏提一點,網上不少博客說到了一個macrotask(宏任務)其實跟這個task(任務)是一個東西。你能夠參考換一下HTML5規範的文檔,裏面甚至沒有macrotask這個詞,「宏」這個概念,只是爲了更好區分任務和微任務的關係。

經過仔細閱讀文檔得知,這兩個概念屬於對異步任務的分類,不一樣的API註冊的異步任務會依次進入自身對應的隊列中,而後等待Event Loop將它們依次壓入執行棧中執行。

task主要包含主代碼setTimeoutsetIntervalsetImmediateI/OUI交互事件

microtask主要包含Promiseprocess.nextTickMutaionObserver

這裏提一點:Promise的then方法,會將傳入的回調函數,加入到microtask queue中。

而後接下來,你須要知道Event Loop的每一個循環的流程:

  • 執行一次最舊的task
  • 而後檢測microtask(微任務),直到全部微任務清空爲止
  • 執行UI render(JS引擎線程被掛起等待,GUI渲染線程開始運行)

如今帶着渲染進程的知識,結合這個流程,來捋一遍上面代碼:

  1. 第一輪循環,主代碼是一個task,因而它進入JS引擎線程中的執行棧開始執行。
  2. setTimeout方法被調用,也進入執行棧,將一個延時的異步事件交給了定時器觸發線程去讀數,而後它立刻退出執行棧。
  3. Promise.resolve()進入執行棧,返回一個Promise,退出執行棧。
  4. then()方法進入執行棧,將一個函數加入了microtask queue(微任務隊列),退出執行棧。
  5. console.log(3)進入執行棧,輸出3,退出執行棧。
  6. 此時,主代碼已經執行完畢,第一個task,退出執行棧。
  7. 而後,執行棧去看microtask queue,發現一個()=>{ console.log(2) }函數,壓入執行棧,輸出2,退出執行棧。
  8. 此時,microtask queue被清空,切到GUI線程,看是有須要變更UI的,第一輪循環完畢。
  9. 第二輪循環。在第一輪循環的代碼執行中,setTimeout發起的定時器是在定時器觸發線程併發進行,讀數完畢,回調交給事件觸發線程中的task queue。因此,此時任務隊列中有一個待執行的task
  10. 執行棧將這個task壓入執行棧執行,輸出1,而後退出執行棧。
  11. 整個Event Loop,繼續重複上面的流程執行。

Event Loop的不斷循環,保證了咱們的JS代碼同步和異步代碼的有序執行。

如今回答一下上面提出的兩個問題:

  1. 由於Javascript設計之初就是一門單線程語言,所以爲了實現主線程的不阻塞,Event Loop這樣的方案應運而生。
  2. task=>microtask=>GUI

重點說一下microtask(微任務)

ES6新引入了Promise標準,同時瀏覽器實現上多了一個microtask微任務概念。在瀏覽器上,主要有兩個微任務API:

  • Promise.then
  • mutation observer

第一個你們應該都熟悉,第二個呢,我以前也不知道,是後來再看Vue的nextTick源碼中看到的,有興趣的同窗能夠去了解一下這個API。

這裏主要說一下微任務和宏任務的不一樣點,和相同點。

不一樣點:

  • 宏任務:
    • 異步的任務是須要在其餘線程上去執行的,主要是爲了保證主線程不阻塞。
    • 宏任務的回調函數,是先保存在任務隊列中的,也就是事件觸發線程上。
    • 一次循環,只執行一個task
  • 微任務:
    • 微任務它不是異步任務,它會直接將回調函數加入microtask queue(微任務隊列)。
    • 每次前一個task執行完畢,而後全部的microtask都要被執行完。

相同點:

  1. 他們的回調函數,都不會本輪循環中當即執行。
  2. 沒有回調函數,它們都將失去意義。

如今再來把最開始提出的後三個問題回顧一下,應該有一個大體的概念了吧。

最後

其實原本是想寫,結合Event Loop來理解Vue的異步批量更新以及nextTcik的,可是後面發現Event Loop這塊寫的太多了,因而就分開寫了。。。

可是,我相信你看完上面的所有內容,在面試的時候,或者碰到異步相關問題的時候,都應該可以應付了。其實這個Event Loop中還有一些用戶交互事件沒詳細講到,有興趣的能夠自行研究一下。

Tips:若是有錯誤或者有歧義的地方,能夠在直接指出。

本篇參考的資料:

「硬核JS」一次搞懂JS運行機制(這篇博客中,說到關於瀏覽器進程和線程的知識,講解的很是詳細,同時對Event Loop也是總結的很是好)

Event Loop的規範和實現(這個主要講Event Loop,也是很是的通俗易懂,裏面的許多案例值得參考)

相關文章
相關標籤/搜索