node基礎面試事件環?微任務、巨集任務?一篇帶你飛

上帝的眼發表於2018-06-29

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

培育能力的事必須繼續不斷地去做,又必須隨時改善學習方法,提高學習效率,才會成功。 —— 葉聖陶

一、我們為什麼要使用node,它的好處是什麼?

Node的首要目標是提供一種簡單的,用於建立高效能伺服器的開發工具。還要解決web伺服器高併發的使用者請求。

解決高併發?

我們這裡來舉個例子,我們node和java相比,在同樣的請求下誰更佔優一點。看圖

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

  • 當使用者請求量增高時,node相對於java有更好的處理併發效能,它可以快速通過主執行緒繫結事件。java每次都要建立一個執行緒,雖然java現在有個執行緒池的概念,可以控制執行緒的複用和數量。
  • 非同步i/o操作,node可以更快的運算元據庫。java訪問資料庫會遇到一個並行的問題,需要新增一個鎖的概念。我們這裡可以打個比方,下課去飲水機接水喝,java是一下子有喝多人去接水喝,需要等待,node是每次都只去一個人接水喝。
  • 密集型CPU運算指的是邏輯處理運算、壓縮、解壓、加密、解密,node遇到CPU密集型運算時會阻塞主執行緒(單執行緒),導致其下面的時間無法快速繫結,所以node不適用於大型密集型CPU運算案例,而java卻很適合。

node在web端場景?

web端場景主要是使用者的請求或者讀取靜態資源什麼的,很適合node開發。應用場景主要有聊天伺服器電子商務網站等等這些高併發的應用。

二、node是什麼?

Node.js是一個基於 Chrome V8 引擎的JavaScript執行環境(runtime),Node不是一門語言,是讓js執行在後端的執行時,並且不包括javascript全集,因為在服務端中不包含DOMBOM,Node也提供了一些新的模組例如http,fs模組等。Node.js 使用了事件驅動、非阻塞式 I/O的模型,使其輕量又高效並且Node.js 的包管理器 npm,是全球最大的開源庫生態系統。

總而言之,言而總之,它只是一個執行時,一個執行環境。

node特性

  • 主執行緒是單執行緒(非同步),將後續的邏輯寫成函式,傳入到當前執行的函式中,當執行的函式得到了結果後,執行傳入的函式(回撥函式)
  • 五個人同時吃一碗飯(非同步)。
  • 阻塞不能非同步(現在假定資料庫是廚師,服務員是node,顧客是請求,一般是廚師做菜讓一個服務員遞給多個使用者,如果廚師邀請服務員聊天,就會導致阻塞,並且是針對核心說的)。
  • i/o操作,讀寫操作,非同步讀寫(能用非同步絕不用同步) 非阻塞式i/o,即可以非同步讀寫。
  • event-driven事件驅動(釋出訂閱)。

node的程式與執行緒

程式是作業系統分配資源和排程任務的基本單位,執行緒是建立在程式上的一次程式執行單位,一個程式上可以有多個執行緒。

在此之前我們先來看看瀏覽器的程式機制

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

自上而下,分別是:

  • 使用者介面--包括位址列、書籤選單等
  • 瀏覽器引擎--使用者介面和渲染引擎之間的傳送指令(瀏覽器的主程式)
  • 渲染引擎--瀏覽器的核心,如(webkit,Gecko)
  • 其他--網路請求,js執行緒和ui執行緒

從我們的角度來看,我們更關心的是瀏覽器的渲染引擎,讓我們往下看。

渲染引擎

  • 渲染引擎是多執行緒的,包含ui執行緒和js執行緒。ui執行緒和js執行緒會互斥,因為js執行緒的執行結果會影響ui執行緒,ui更新會被儲存在佇列,直到js執行緒空閒,則被取出來更新。
  • js單執行緒是單執行緒的,為什麼呢?假如js是多執行緒的,那麼操作DOM就是多執行緒操作,那樣的話就會很混亂,DOM不知道該聽誰的,而這裡的單執行緒指得是主執行緒是單執行緒的,他同樣可以有非同步執行緒,通過佇列存放這些執行緒,而主執行緒依舊是單執行緒,這個我們後面再講。所以在node中js也是單執行緒的。
  • 單執行緒的好處就是節約記憶體,不需要再切換的時候執行上下文,也不用管鎖的概念,因為我們每次都通過一個。

三、瀏覽器中的Event Loop

這裡我先要說一下瀏覽器的事件環,可能有人會說,你這篇文章明明是講node的怎麼會扯到瀏覽器。首先他們都是以js為底層語言的不同執行時,有其相似之處,再者多學一點也不怕面試官多問。好了我廢話不多說,開始。

首先我們需要知道堆,棧和佇列的關係和意義。

  • 堆(heap):堆是存放物件的一個空間(Object、function)
  • 佇列(loop):是指存放所有非同步請求操作的結果,直到有一個非同步操作完成它的使命,就會在loop中新增一個事件,佇列是先進先出的,比如下面的圖,最先進佇列的會先被打出去

隔山打牛!

  • 棧(stack):棧本身是儲存基礎的變數,比如1,2,3,還有引用的變數,這裡可能有人會問你上面的堆不是存放引用型別的物件嗎,怎麼變棧裡去了。這裡我要解釋一下,因為棧裡面的存放的引用變數是指向堆裡的引用物件的地址只是一串地址。這裡棧代表的是執行棧,我們js的主執行緒。棧是先進後出的,先進後出就是相當於喝水的水杯,我們倒水進去,理論上喝到的水是最後進水杯的。我們可以看程式碼,follow me
function a(){
  console.log('a')
  function b(){
    console.log('b')    
    function c(){
      console.log('c')
    }
    c()
  }
  b()
}
a()

//這段程式碼是輸出a,b,c,執行棧中的順序的c,b,a,如果是遵循先進先出,就是輸出c,b,a。所以棧先進後出這個特性大家要牢記。
複製程式碼

OK,現在大家已經知道堆,棧和佇列的關係,現在我們來看一張圖。

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

我分析一下這張圖

  • 我們的同步任務在主執行緒上執行會形成一個執行棧
  • 如果碰到非同步任務,比如setTimeout、onClick等等的一些操作,我們會將他的執行結果放入佇列,此期間主執行緒不阻塞
  • 等到主執行緒中的所有同步任務執行完畢,就會通過event loop在佇列裡面從頭開始取,在執行棧中執行
  • event loop永遠不會斷
  • 以上的這一整個流程就是Event Loop(事件迴圈機制)

微任務、巨集任務?

macro-task(巨集任務): setTimeout,setImmediate,MessageChannel micro-task(微任務): 原生Promise(有些實現的promise將then方法放到了巨集任務中),Object.observe(已廢棄), MutationObserver

微任務和巨集任務皆為非同步任務,它們都屬於一個佇列,主要區別在於他們的執行順序,Event Loop的走向和取值。那麼他們之間到底有什麼區別呢

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

每次執行棧的同步任務執行完畢,就會去任務佇列中取出完成的非同步任務,佇列中又分為microtasks queues和巨集任務佇列等到把microtasks queues所有的microtasks都執行完畢,注意是所有的,他才會從巨集任務佇列中取事件。等到把佇列中的事件取出一個,放入執行棧執行完成,就算一次迴圈結束,之後event loop還會繼續迴圈,他會再去microtasks queues執行所有的任務,然後再從巨集任務佇列裡面取一個,如此反覆迴圈。

  • 同步任務執行完
  • 去執行microtasks,把所有microtasks queues清空
  • 取出一個macrotasks queues的完成事件,在執行棧執行
  • 再去執行microtasks
  • ...
  • ...
  • ...

我這麼說可能大家會有點懵,不慌,我們來看一道題

setTimeout(()=>{
  console.log('setTimeout1')
},0)
let p = new Promise((resolve,reject)=>{
  console.log('Promise1')
  resolve()
})
p.then(()=>{
  console.log('Promise2')    
})
複製程式碼

最後輸出結果是Promise1,Promise2,setTimeout1

  • Promise引數中的Promise1是同步執行的,Promise還不是很瞭解的可以看看我另外一篇文章Promise之你看得懂的Promise,
  • 其次是因為Promise是microtasks,會在同步任務執行完後會去清空microtasks queues
  • 最後清空完微任務再去巨集任務佇列取值
Promise.resolve().then(()=>{
  console.log('Promise1')  
  setTimeout(()=>{
    console.log('setTimeout2')
  },0)
})

setTimeout(()=>{
  console.log('setTimeout1')
  Promise.resolve().then(()=>{
    console.log('Promise2')    
  })
},0)
複製程式碼

這回是巢狀,大家可以看看,最後輸出結果是Promise1,setTimeout1,Promise2,setTimeout2

  • 一開始執行棧的同步任務執行完畢,會去microtasks queues
  • 清空microtasks queues,輸出Promise1,同時會生成一個非同步任務setTimeout1
  • 巨集任務佇列檢視此時佇列是setTimeout1在setTimeout2之前,因為setTimeout1執行棧一開始的時候就開始非同步執行,所以輸出setTimeout1,在執行setTimeout1時會生成Promise2的一個microtasks,放入microtasks queues
  • 接著又是一個迴圈,去清空microtasks queues,輸出Promise2
  • 清空完microtasks queues,就又會去巨集任務佇列取一個,這回取的是setTimeout2

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

四、node中的事件環

node的事件環相比瀏覽器就不一樣了,我們先來看一張圖,他的工作流程

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

  • 首先我們能看到我們的js程式碼(APPLICATION)會先進入v8引擎,v8引擎中主要是一些setTimeout之類的方法。
  • 其次如果我們的程式碼中執行了nodeApi,比如require('fs').read(),node就會交給libuv庫處理,這個libuv庫是別人寫的,他就是node的事件環。
  • libuv庫是通過單執行緒非同步的方式來處理事件,我們可以看到work threads是個多執行緒的佇列,通過外面event loop阻塞的方式來進行非同步呼叫。
  • 等到work threads佇列中有執行完成的事件,就會通過EXECUTE CALLBACK回撥給EVENT QUEUE佇列,把它放入佇列中。
  • 最後通過事件驅動的方式,取出EVENT QUEUE佇列的事件,交給我們的應用

node中的event loop

node中的event loop是在libuv裡面的,libuv裡面有個事件環機制,他會在啟動node時,初始化事件環

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

  • 這裡的每一個階段都對應著一個事件佇列
  • 每當event loop執行到某個階段時,都會執行對應的事件佇列中的事件,依次執行
  • 當該佇列執行完畢或者執行數量超過上限,event loop就會執行下一個階段
  • 每當event loop切換一個執行佇列時,就會去清空microtasks queues,然後再切換到下個佇列去執行,如此反覆

這裡我們要注意setImmediate是屬於check佇列的,還有poll佇列主要是非同步的I/O操作,比如node中的fs.readFile()

我們來具體看一下他的用法吧

setImmediate(()=>{
  console.log('setImmediate1')
  setTimeout(()=>{
    console.log('setTimeout1')    
  },0)
})
setTimeout(()=>{
  console.log('setTimeout2') 
  process.nextTick(()=>{console.log('nextTick1')})
  setImmediate(()=>{
    console.log('setImmediate2')
  })   
},0)
複製程式碼
  • 首先我們可以看到上面的程式碼先執行的是setImmediate1,此時event loopcheck佇列
  • 然後setImmediate1從佇列取出之後,輸出setImmediate1,然後會將setTimeout1執行
  • 此時event loop執行完check佇列之後,開始往下移動,接下來執行的是timers佇列
  • 這裡會有問題,我們都知道setTimeout1設定延遲為0的話,其實還是有4ms的延遲,那麼這裡就會有兩種情況。先說第一種,此時setTimeout1已經執行完畢
    • 根據node事件環的規則,我們會執行完所有的事件,即取出timers佇列中的setTimeout2,setTimeout1
    • 此時根據佇列先進先出規則,輸出順序為setTimeout2,setTimeout1,在取出setTimeout2時,會將一個process.nextTick執行(執行完了就會被放入微任務佇列),再將一個setImmediate執行(執行完了就會被放入check佇列
    • 到這一步,event loop會再去尋找下個事件佇列,此時event loop會發現微任務佇列有事件process.nextTick,就會去清空它,輸出nextTick1
    • 最後event loop找到下個有事件的佇列check佇列,執行setImmediate,輸出setImmediate2
  • 假如這裡setTimeout1還未執行完畢(4ms耽誤了它的終身大事?)
    • 此時event loop找到timers佇列,取出*timers佇列**中的setTimeout2,輸出setTimeout2,把process.nextTick執行,再把setImmediate執行
    • 然後event loop需要去找下一個事件佇列,這裡大家要注意一下,這裡會發生2步操作,1、setTimeout1執行完了,放入timers佇列。2、找到微任務佇列清空。,所以此時會先輸出nextTick1
    • 接下來event loop會找到check佇列,取出裡面已經執行完的setImmediate2
    • 最後event loop找到timers佇列,取出執行完的setTimeout1這種情況下event loop比上面要多切換一次

所以有兩種答案

  1. setImmediate1,setTimeout2,setTimeout1,nextTick1,setImmediate2
  2. setImmediate1,setTimeout2,nextTick1,setImmediate2,setTimeout1

node基礎面試事件環?微任務、巨集任務?一篇帶你飛

這裡的圖只參考了第一種情況,另一種情況也類似

五、node的同步、非同步,阻塞、非阻塞

  • 同步:即為呼叫者等待被呼叫者這個過程,如果被呼叫者一直不反回結果,呼叫者就會一直等待,這就是同步,同步有返回值
  • 非同步:即為呼叫者不等待被呼叫者是否返回,被呼叫者執行完了就會通過狀態、通知或者回撥函式給呼叫者,非同步沒有返回值
  • 阻塞:指代當前執行緒在結果返回之前會被掛起,不會繼續執行下去
  • 非阻塞: 即當前執行緒不管你返回什麼,都會繼續往下執行

有些人可能會搞亂他們之間的關係,同步、非同步是被呼叫者的狀態,阻塞、非阻塞是呼叫者的狀態、訊息

接下來我們來看看他們的組合會是怎麼樣的

組合 意義
同步阻塞 這就相當於我去飯店吃飯,我需要在廚房等待菜燒好了,才能吃。我是呼叫者我需要等待上菜於是被阻塞,菜是被呼叫者做好直接給我是同步
非同步阻塞 我去飯店吃飯,我需要等待菜燒好了才能吃,但是廚師有事,希望之後處理完事能做好之後通知我去拿,我作為呼叫者等待就是阻塞的,而菜作為被呼叫者是做完之後通知我的,所以是非同步的,這種方式一般沒用。
同步非阻塞 我去飯店吃飯,先叫了碗熱菜,在廚房等廚師做菜,但我很餓,就開始吃廚房冷菜,我是呼叫者我沒等熱菜好就開始吃冷菜,是非阻塞的,菜作為被呼叫者做好直接給我是同步的,這種方式一般也沒人用
非同步非阻塞 我去飯店吃飯。叫了碗熱菜,廚師在做菜,但我很餓,先吃冷菜,廚師做好了通知我去拿,我是呼叫者我不會等熱菜燒好了再吃冷菜,是非阻塞的,菜作為被呼叫者通知我拿是非同步的

結尾

希望大家看了本篇文章都有收穫,這樣出去面試的時候就不會這樣

node基礎面試事件環?微任務、巨集任務?一篇帶你飛
而是這樣。好了,最後希望大家世界盃都能夠逢賭必贏,自己喜歡的球隊也能夠殺進決賽
node基礎面試事件環?微任務、巨集任務?一篇帶你飛

相關文章