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

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

1、咱們爲何要使用node,它的好處是什麼?

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

解決高併發?

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

  • 當用戶請求量增高時,node相對於java有更好的處理併發性能,它能夠快速經過主線程綁定事件。java每次都要建立一個線程,雖然java如今有個線程池的概念,能夠控制線程的複用和數量。
  • 異步i/o操做,node能夠更快的操做數據庫。java訪問數據庫會遇到一個並行的問題,須要添加一個鎖的概念。咱們這裏能夠打個比方,下課去飲水機接水喝,java是一會兒有喝多人去接水喝,須要等待,node是每次都只去一我的接水喝。
  • 密集型CPU運算指的是邏輯處理運算、壓縮、解壓、加密、解密,node遇到CPU密集型運算時會阻塞主線程(單線程),致使其下面的時間沒法快速綁定,因此node不適用於大型密集型CPU運算案例,而java卻很適合。

node在web端場景?

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

2、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的進程與線程

進程是操做系統分配資源和調度任務的基本單位,線程是創建在進程上的一次程序運行單位,一個進程上能夠有多個線程。npm

在此以前咱們先來看看瀏覽器的進程機制後端

自上而下,分別是:promise

  • 用戶界面--包括地址欄、書籤菜單等
  • 瀏覽器引擎--用戶界面和渲染引擎之間的傳送指令(瀏覽器的主進程)
  • 渲染引擎--瀏覽器的內核,如(webkit,Gecko)
  • 其餘--網絡請求,js線程和ui線程

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

渲染引擎

  • 渲染引擎是多線程的,包含ui線程和js線程。ui線程和js線程會互斥,由於js線程的運行結果會影響ui線程,ui更新會被保存在隊列,直到js線程空閒,則被取出來更新。
  • js單線程是單線程的,爲何呢?假如js是多線程的,那麼操做DOM就是多線程操做,那樣的話就會很混亂,DOM不知道該聽誰的,而這裏的單線程指得是主線程是單線程的,他一樣能夠有異步線程,經過隊列存放這些線程,而主線程依舊是單線程,這個咱們後面再講。因此在node中js也是單線程的。
  • 單線程的好處就是節約內存,不須要再切換的時候執行上下文,也不用管鎖的概念,由於咱們每次都經過一個。

3、瀏覽器中的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,如今你們已經知道堆,棧和隊列的關係,如今咱們來看一張圖。

我分析一下這張圖

  • 咱們的同步任務在主線程上運行會造成一個執行棧
  • 若是碰到異步任務,好比setTimeout、onClick等等的一些操做,咱們會將他的執行結果放入隊列,此期間主線程不阻塞
  • 等到主線程中的全部同步任務執行完畢,就會經過event loop在隊列裏面從頭開始取,在執行棧中執行
  • event loop永遠不會斷
  • 以上的這一整個流程就是Event Loop(事件循環機制)

微任務、宏任務?

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

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

每次執行棧的同步任務執行完畢,就會去任務隊列中取出完成的異步任務,隊列中又分爲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

4、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時,初始化事件環

  • 這裏的每個階段都對應着一個事件隊列
  • 每當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步操做,一、setTimeout1執行完了,放入timers隊列。二、找到微任務隊列清空。,因此此時會先輸出nextTick1
    • 接下來event loop會找到check隊列,取出裏面已經執行完的setImmediate2
    • 最後event loop找到timers隊列,取出執行完的setTimeout1這種狀況下event loop比上面要多切換一次

因此有兩種答案

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

這裏的圖只參考了第一種狀況,另外一種狀況也相似

5、node的同步、異步,阻塞、非阻塞

  • 同步:即爲調用者等待被調用者這個過程,若是被調用者一直不反回結果,調用者就會一直等待,這就是同步,同步有返回值
  • 異步:即爲調用者不等待被調用者是否返回,被調用者執行完了就會經過狀態、通知或者回調函數給調用者,異步沒有返回值
  • 阻塞:指代當前線程在結果返回以前會被掛起,不會繼續執行下去
  • 非阻塞: 即當前線程無論你返回什麼,都會繼續往下執行

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

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

組合 意義
同步阻塞 這就至關於我去飯店吃飯,我須要在廚房等待菜燒好了,才能吃。我是調用者我須要等待上菜因而被阻塞,菜是被調用者作好直接給我是同步
異步阻塞 我去飯店吃飯,我須要等待菜燒好了才能吃,可是廚師有事,但願以後處理完事能作好以後通知我去拿,我做爲調用者等待就是阻塞的,而菜做爲被調用者是作完以後通知個人,因此是異步的,這種方式通常沒用。
同步非阻塞 我去飯店吃飯,先叫了碗熱菜,在廚房等廚師作菜,但我很餓,就開始吃廚房冷菜,我是調用者我沒等熱菜好就開始吃冷菜,是非阻塞的,菜做爲被調用者作好直接給我是同步的,這種方式通常也沒人用
異步非阻塞 我去飯店吃飯。叫了碗熱菜,廚師在作菜,但我很餓,先吃冷菜,廚師作好了通知我去拿,我是調用者我不會等熱菜燒好了再吃冷菜,是非阻塞的,菜做爲被調用者通知我拿是異步的

結尾

但願你們看了本篇文章都有收穫,這樣出去面試的時候就不會這樣

而是這樣。好了,最後但願你們世界盃都可以 逢賭必贏,本身喜歡的球隊也可以 殺進決賽
相關文章
相關標籤/搜索