小邵教你玩轉nodejs之nodejs概念、事件環機制(1)

前言:你們好,我叫邵威儒,你們都喜歡喊我小邵,學的金融專業卻憑藉興趣愛好入了程序猿的坑,從大學買的第一本vb和自學vb,我就與編程結下不解之緣,隨後自學易語言寫遊戲輔助、交易軟件,至今進入了前端領域,看到很多朋友都寫文章分享,本身也弄一個玩玩,如下文章純屬我的理解,便於記錄學習,確定有理解錯誤或理解不到位的地方,意在站在前輩的肩膀,分享我的對技術的通俗理解,共同成長!javascript

後續我會陸陸續續更新javascript方面,儘可能把javascript這個學習路徑體系都寫一下
包括前端所經常使用的es六、angular、react、vue、nodejs、koa、express、公衆號等等
都會從淺到深,從入門開始逐步寫,但願能讓你們有所收穫,也但願你們關注我~php

文章列表:juejin.im/user/5a84f8…html

Author: 邵威儒
Email: 166661688@qq.com
Wechat: 166661688
github: github.com/iamswr/前端


接下來會寫nodejs連載的筆記,本文主要是講nodejs解決了什麼問題有什麼優點、進程與線程的概念、同步與異步的概念、阻塞與非阻塞的概念、隊列和棧的概念、宏任務和微任務以及很是重要的瀏覽器的事件環和nodejs的事件環(event loop)。vue


Node解決了什麼問題,有什麼優點?

咱們前端和後端交互,主要是請求個接口或者讓後端返回個頁面,頻繁進行io操做,web服務最大的瓶頸就是處理高併發(同一時間併發訪問服務器的數量),而node則在高併發、io密集型的場景,有明顯的優點。java

i/o密集型是指文件操做、網絡操做、讀取等操做;
cpu密集型則是指須要進行大量的邏輯處理運算、加解密、壓縮解壓等操做;node

node是什麼?

Node.js是一個基於 Chrome V8 引擎的JavaScript運行環境(runtime)。mysql

雖然node是用javascript的語法,可是並不是是徹底的javascript,咱們知道javascript是包含了ECMAScript、DOM、BOM,而node則不包含DOM和BOM,可是它也提供了一系列模塊供咱們使用,如http、fs模塊。react

node是使用了事件驅動非阻塞式 I/O的模型,使其輕量並且高效,咱們在開發中,會大量接觸到node的第三方模塊包,以及node擁有全球最大的開源庫生態系統。c++

事件驅動:發送事件後,經過回調的消息通知機制通知;
非阻塞式 I/O:如操做文件,經過非阻塞異步的方式讀取文件;


進程和線程

假設咱們使用java、php等服務器

通常啓服務器用tomcat(apache)、iis,屬於多線程同步阻塞,而後啓動服務的時候會配置線程數。

mysql、mongo、redis等則是數據庫。

通常是從客戶端發起請求給服務器,服務器操做數據庫,而後數據庫把數據返給服務器,服務器再返回給客戶端。

當客戶端發起請求到服務器時,服務器會有線程來處理這條請求,假如是tomcat iis等是屬於多線程同步阻塞的,在起服務的時候會配置線程數,而後再經過服務器發送請求到數據庫請求數據,此時該線程會一直等待數據庫的數據返回,當數據庫返回數據給服務器後,服務器再把數據返回給客戶端。

當併發量很大時,請求超過線程數時,則排在後面的請求會等待前面的請求完成後纔會執行,線程完成後,並非立刻銷燬,再建立,而是完成上一次請求後,會被複用到下一個請求當中。

圖中顯示的外層方形,爲進程,內層方形爲線程,一個進程能夠分配多個線程,咱們實際開發中,一個項目通常是多進程。

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

假如咱們使用node

那麼nodejs,咱們說了是單線程,並非說一個進程裏面只能跑一條線程,而是主線程是單線程,node是如上圖這樣的。

當客戶端同時發送請求時,第一條請求發送到服務器後會有一條線程處理,服務器會請求數據庫,此時線程並不像上面那種方式,在等待數據的返回,該線程而是去處理第二條請求,當數據庫返回第一條數據時,該線程再經過callback、事件環等機制執行。

雖然node是單線程,可是能夠經過setTimeout開啓多個線程,當併發量很高的時候能夠這樣玩。

可是node並非什麼場景都能使用的,對於cpu密集型的場景,反而不太實用,cpu密集型是指須要大量邏輯、計算,好比大量計算、壓縮、加密、解密等(這部分c++優點大),node比較適合的是io操做(io密集),爲何說node適合前端?由於前端主要就是請求個接口,或者服務器渲染返回頁面,因此node會很是適合前端這種場景。

咱們經常使用node做爲中間層,客戶端訪問node,而後由node去訪問服務器,好比java層,java把數據返回給node,node再把數據返回給客戶端。

咱們常見的java是同步多線程,node是異步單線程,若是說java的併發量是1000萬,那麼node併發量能夠達到3倍以上。

那麼咱們接下來了解一下常常和前端打交道的瀏覽器進程、線程

  • User Interface(用戶界面 進程):如地址欄、標籤、前進後退等;
  • Browser engine(瀏覽器引擎 瀏覽器的主進程):在用戶界面和渲染引擎之間傳達指令;
  • Data Persistence(持久層 進程):存放cookies、sessionStorage、loaclStorage、indexedDB等;
  • Rendering engine(渲染引擎 進程):渲染引擎內部是多線程的,其中有Networking(ajax請求)、JavaScript Interpreter(js線程)、UI Backend(UI線程)

在渲染引擎中,須要注意的是,在其內部有兩個很是重要的線程,就是js線程和ui線程,js線程和ui線程是互斥的,共用同一條線程。

那麼爲何js線程和ui線程是互斥的?爲何是單線程?咱們能夠設想一下,當咱們經過js操做一個DOM節點的時候,若是同時執行,那麼就存在快慢之分,會顯得很混亂,再設想一下,若是是多線程的話,多條線程操做同一個DOM節點,是否是也顯得很混亂?因此js設計爲單線程。

衍生一下,使用java時,若是多線程訪問某一個一樣的資源,往 往會給這個資源加一把鎖,有點相似下課了,多個同窗上廁所,而廁所只有一間,先進去的人把門鎖上了,後面的人只能排隊,可是nodejs就基本不用擔憂這個問題。

js單線程指的是js的主線程是單線程。

瀏覽器中還有其餘線程
  • 瀏覽器事件觸發線程
  • 定時器觸發線程
  • 異步HTTP請求觸發線程

異步和同步、阻塞和非阻塞

主要分爲如下幾類組合

  • 同步阻塞
  • 異步阻塞
  • 同步非阻塞
  • 異步非阻塞

假設調用方爲小明,被調用方爲小紅 圖1:小明喜歡小紅,小明因而乎決定給小紅打電話表白,小紅接電話,若是此時小紅把電話晾在那,小明則有兩種狀態,一種是阻塞、一種是非阻塞,阻塞就是小紅晾電話的同時,小明還在等着,叫作阻塞,非阻塞就是小紅晾電話的同時,小明能夠去幹別的事情,叫作非阻塞,小紅接電話後說,我要想想再給你答覆,此時若是沒掛掉電話,那麼是在同步,若是掛掉電話一會再告訴小明,那麼就會是異步。

圖2:當小紅接電話後,說想想,一會再告訴你結果,而後把電話掛了,此時屬於異步,而後小明若是還在癡情地等待電話回覆(即2.1),那麼稱爲阻塞,若是小明此時並非乾等這個答覆,而是打電話向另一個妹子表白(即2.2),那麼稱爲非阻塞,結合起來就是異步堵塞或異步非堵塞。

圖3:當小紅接電話後,說想想,一會再告訴你結果,而後電話也不掛,一直通話,此時屬於同步,可是小明此時偷偷向另一個妹子打電話表白,這個行爲屬於非堵塞,結合起來就是同步非阻塞。


隊列和棧

  • 隊列的特色:隊列的特色是先進先出,如數組,依次日後添加。

  • 棧的特色則是先進後出

首先咱們往棧裏分別放一、二、3進去,而後取出時,是按照3 2 1取出

function a() {
  function b() {
    function c() {

    }
    c()
  }
  b()
}

a()

// 這個代碼中,咱們是依次執行了a函數、b函數、c函數
// 可是在銷燬的時候,是先從c函數銷燬,而後再銷燬b函數
// 最後銷燬a函數,若是是先銷燬a函數的話,那麼b就會失去了其執行棧
// 也就是執行上下文,因此在執行棧中,是先進後出。
複製代碼

宏任務、微任務(都屬於異步操做,暫時以瀏覽器事件環機制來說)

你們都知道異步,可是在異步當中,又分爲兩大類,即宏任務、微任務,在瀏覽器事件環當中,微任務是在宏任務執行以前執行的。

常見的宏任務:

  • setTimeout
  • setImmediate(只有ie支持)
  • setInterval
  • messageChannel

常見的微任務:

  • Promise.then()
  • mutationObserver
咱們在使用vue的時候,有一個nextTick方法,意思是把一個方法插入到下一個隊列當中,咱們能夠看看它的源碼是怎樣實現的

源碼:github.com/vuejs/vue/b…

if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
複製代碼

在這段代碼中,能夠看出vue的nextTick對於宏任務的處理,首先是判斷是否有setImmediate,若是沒有的話,則判斷是否有MessageChannel,若是尚未的話,最後降級爲setTimeout。

if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  microTimerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
} else {
  // fallback to macro
  microTimerFunc = macroTimerFunc
}
複製代碼

在這段代碼中,能夠看出vue的nextTick對於微任務的處理,首先是判斷是否有Promise,若是沒有Promise的話,則降級爲宏任務。

setImmediate

接下來咱們看下這段代碼如何執行(注意setImmediate須要ie瀏覽器打開)

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script>
    setImmediate(function(){
      console.log('我是setImmediate')
    },0)

    Promise.resolve().then(function(){
      console.log('我是Promise')
    })
    console.log('我是同步代碼')
  </script>
</body>
</html>
複製代碼

依次打印出 '我是同步代碼' -> '我是Promise' -> '我是setImmediate'
能夠看出執行順序是先執行同步代碼,而後微任務的代碼,而後宏任務的代碼。

MessageChannel

如下代碼依次打印出 "我是同步代碼" -> "hello swr",由於MessageChannel也是宏任務。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <script>
    let messageChannel = new MessageChannel()
    let port1 = messageChannel.port1
    let port2 = messageChannel.port2
    port1.postMessage('hello swr')
    port2.onmessage = function (data) {
      console.log(data.data)
    }
    console.log('我是同步代碼')
  </script>
</body>
</html>
複製代碼

MutationObserver

MutationObserve主要是用於監控DOM節點的更新,好比咱們有個需求,但願插入DOM完成後,才執行某些行爲,咱們就能夠這樣作。

首先會打印"我是同步代碼",而後執行兩個for循環插入dom節點,最終dom節點更新完畢後,會打印「插入完成 100」。

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
  <div id='div'>
    
  </div>
  <script>
    let observe = new MutationObserver(function(){
      // 這一步之因此打印出p的數量,是爲了驗證插完dom節點後,才執行這一步。
      console.log('插入完成',document.querySelectorAll('p').length)
    })
    observe.observe(div,{childList:true})
    console.log('我是同步代碼')
    for(let i = 0 ;i < 50;i++){
      div.appendChild(document.createElement('p'))
    }
    for(let i = 0 ;i < 50;i++){
      div.appendChild(document.createElement('p'))
    }
  </script>
</body>
</html>
複製代碼

宏任務和微任務如何執行

首先咱們看一段代碼

setTimeout(() => {
  console.log('我是setTimeout1')
  Promise.resolve().then(()=>{
    console.log('我是Promise1')
  })
}, 0);

Promise.resolve().then(()=>{
  console.log('我是Promise2')
  setTimeout(() => {
    console.log('我是setTimeout2')
  }, 0);
  Promise.resolve().then(()=>{
    console.log('我是Promise3')
  })
})
複製代碼

在這段代碼中,咱們要弄明白,什麼是宏任務,什麼是微任務,
setTimeout是宏任務,而Promise.resolve().then()是微任務。

還有個概念就是特別須要注意的,這也是和node.js有所區別,
在瀏覽器事件環機制中,當執行棧的同步代碼清空後,系統會去讀取任務隊列,其中會優先讀取微任務,把微任務清空後,再依次讀取宏任務,這裏特別注意,並不是一次性執行完全部宏任務,而是像隊列那樣,先取一個宏任務執行,執行完後,再去看是否有微任務,若是有,則執行微任務,而後再讀取一個宏任務執行,不斷循環。

這裏須要注意的是,在nodejs的事件環機制中,是優先執行微任務,可是當執行完微任務,進入宏任務的時候,即便在執行宏任務過程當中存在新的微任務,也不會優先執行微任務,而是把宏任務隊列中執行完畢。

首先,代碼會從上到下執行,碰到setTimeout1,會丟到宏任務隊列中,而後往下執行遇到Promise2,那麼在執行棧執行完畢後,會優先執行Promise2,打印出「我是Promise2」,執行了Promise2後,發現裏面有個setTimeout2,此時會把setTimeout2丟到宏任務隊列中,而後繼續往下執行,會碰到Promise3,此時會把Promise3丟到微任務中,而且執行,打印出「我是Promise3」,而後此時微任務隊列執行完畢了,會去宏任務中讀setTimeout1出來執行,打印出「我是setTimeout1」,再繼續執行,發現裏面有個Promise1,那麼此時會把Promise1丟到微任務中,而且執行Promise1,打印出「我是Promise1」,此時微任務隊列又清空了,再去宏任務隊列中取出setTimeout2而且執行,打印出「我是setTimeout2」。

打印順序爲'我是Promise2' -> '我是Promise3' -> '我是setTimeout1' -> '我是Promise1' -> '我是setTimeout2'


事件環之瀏覽器的事件環Event Loop

這個圖就是瀏覽器的事件環,JS中分爲兩部分,爲堆(heap)和棧(stack),通常棧,咱們也能夠成爲執行棧、執行上下文,咱們在棧中操做的時候,會發一些好比ajax請求操做、定時器等,能夠看圖中的WebAPIs,是屬於多線程,那麼這個WebAPIs多線程是怎樣放到棧中執行呢?

好比ajax請求成功後後,會把ajax的回調放到隊列(callback queue)中,而後當執行棧中把全部的同步任務執行完畢後,系統會讀取隊列中的事件放到執行棧中依次執行,若是執行棧中有同步任務,那麼則會執行同步任務中的任務後再依讀取隊列中的事件到執行棧中依次執行,這個過程是不斷循環的。

總結:

  1. 全部的同步任務在主線程上執行,造成了一個執行棧;
  2. 如在執行棧中有異步任務,那麼當這個異步任務有運行結果後,會放置任務隊列中;
  3. 若是執行棧中的同步任務執行完畢,那麼會從任務隊列中依次讀取事件到執行棧中依次執行;
  4. 執行棧從任務隊列中讀取事件的過程,是不斷循環的;

舉些例子驗證

// 咱們有3個setTimeout
setTimeout(() => {
  console.log('a')
}, 0);

setTimeout(() => {
  console.log('b')
}, 0);

setTimeout(() => {
  console.log('c')
}, 0);
console.log('hello swr')
// 首先會依次從上到下執行代碼,遇到setTimeout異步事件,則放到WebAPIs中
// 若是有告終果(記住,是要有了運行結果!),則會放到任務隊列中
// 而後會執行同步代碼console.log('hello swr')
// 此時的流程是:
// 首先打印出 'hello swr',而後執行棧中同步代碼已經執行完畢,則去任務隊列中
// 依次取出這3個setTimeout的事件執行,依次打印出 'a' 'b' 'c'
// 這個順序永遠都不會亂,由於遵循了事件環的機制
複製代碼

那麼爲何說執行棧中的同步代碼執行完後纔會執行任務隊列中的任務呢?
接下來咱們能夠看看這個例子,假如咱們在同步代碼中寫了死循環,那麼還會執行任務隊列中的事件嗎?

setTimeout(() => {
  console.log('a')
}, 0);

setTimeout(() => {
  console.log('b')
}, 0);

setTimeout(() => {
  console.log('c')
}, 0);
for(;;){} 
// 死循環,咱們發現永遠都不會打印出'a' 'b' 'c'
// 由於同步代碼是死循環,一直處於執行狀態,執行棧中的同步代碼還沒執行完畢
// 是不會去讀取任務隊列中的事件的
複製代碼

事件環之nodejs的事件環Event Loop

nodejs也有它本身的事件環,和瀏覽器事件環的機制並不是都同樣的,咱們寫的應用代碼通常是運行在V8引發裏面,它裏面並不是僅僅是V8引擎裏面的東西,好比setTimeout,好比eval都是V8引擎提供的,咱們寫代碼還會基於一些node api,也就是node.js bindings,好比node的fs模塊,能夠發一些異步的io操做,可是node裏面的異步和瀏覽器的不同,它是本身有一套LIBUV庫,專門處理異步的io操做的,它靠的是多線程實現的(worker threads),它用多線程模擬了異步的機制,咱們每次調用node api的時候,它裏面會進入LIBUV調用多個線程執行,同步堵塞調用,模擬了異步的機制,成功之後,經過callback執行放到一個隊列裏,而後返回給咱們的客戶端。

nodejs事件環,給每一個階段都劃分得很清楚,由於nodejs裏面有libuv庫,裏面有以上這幾個方面,每個都是一個隊列。

在4中,會不斷進行輪詢poll中的i/o隊列和檢查定時器是否到時,若是是的話,會從把這個事件切換到1的隊列中。

在5中,只存放setImmediate,若是4處於輪詢時,發現有check階段,那麼就會往下走進入check階段。

執行順序

  • 首先執行完執行棧中的代碼;
  • 如微任務中有事件,則執行微任務中的全部隊列執行完畢;
  • 執行1中的隊列;
  • 而後依次執行下一個隊列,須要注意的是,這裏對於微任務的處理,和瀏覽器事件環機制不一樣,好比node在執行1中的隊列時,是依次執行完,哪怕中途有新的微任務,也不會執行微任務,而是當這個隊列執行完畢後,切換到下一個隊列以前,才執行微任務;
  • 隊列之間的切換,會執行一次微任務;
  • 這個過程是不斷循環的;

這段代碼能夠看出,在node的事件環中,當執行到1的隊列中時,即便有新的微任務,也不會立刻執行微任務,而是把當前的隊列清空後纔會執行微任務。

setTimeout(() => {
  console.log('setTimeout1')
  process.nextTick(()=>{
    console.log('nextTick')
  })
}, 0);

setTimeout(() => {
  console.log('setTimeout2')
}, 0);

// 依次打印輸出 'setTimeout1' -> 'setTimeout2' -> 'nextTick'
複製代碼

小夥伴提問區:

相關文章
相關標籤/搜索