node 基礎與 Event Loop

node 的特色

  • 主線程是單線程
  • 異步非阻塞(非阻塞I/O)
  • 事件驅動

單線程javascript

  • 進程和線程

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

  • 什麼是單線程?

一個進程中只有一個線程,程序順序執行,前面的執行完成後纔會執行後面的程序。node

有點兒像點菜同樣, 顧客來了。服務員接單,服務員接完單後立刻告訴廚房去作菜吧,此時服務員不會等待菜作好而是立刻會被釋放出來繼續服務下一個客戶。 一直都是一個服務員在工做,等菜作好了,服務員在回去取菜給客戶吃。ajax

單線程特色是節約內存,而且不須要再切換執行上下文,並且單線程不須要考慮鎖的問題。數據庫

以下圖api

image

異步非阻塞promise

  • 同步和異步

同步和異步關注的是消息通知機制,指代的是被調用方瀏覽器

同步就是發出調用後,沒有獲得結果前,該調用不返回,一旦調用返回,就獲得返回值bash

當一個異步過程調用發出後,調用者不會馬上獲得結果,而是調用發出後,被調用者經過狀態、通知或者回調函數處理這個調用。服務器

  • 阻塞和非阻塞

阻塞和非阻塞關注的是程序在等待調用結果(消息、返回值)的狀態,針對的是調用者

阻塞調用是指調用結果返回以前,當前線程會被掛起。調用線程只有在獲得結果後纔會返回

非阻塞調用指在不能馬上獲得結果前,該調用不會阻塞當前線程

a>  同步阻塞
    調用者:我喜歡你 
    被調用者:我也喜歡你
    
b>  異步阻塞
    調用者:  我喜歡你
    被調用者:我和我媽商量下。回頭回覆你
    調用者:  不掛斷電話,我等你回覆
    
c>  異步非阻塞
    調用者: 我喜歡你
    被調用者: 我和我媽商量下。回頭回覆你
    調用者:  掛斷電話,不等你了。聯繫了另一個妹子
    
d>  同步非阻塞
    調用者:給 A 打電話 我喜歡你
    被調用者(A):正準備回覆
    調用者:不掛斷電話 A 轉身又給 B 打電話 我喜歡你
複製代碼
  • I/O 操做

    • 訪問服務器的靜態資源

    • 讀取數據,讀取文件

node 在處理高併發, I/O密集場景有明顯優點。高併發是指在同一時間併發訪問服務器,I/O密集知道是文件操做、網絡操做、數據庫。相對的有 CPU 密集,CPU 密集值的是邏輯處理運算、壓縮、解壓、加密、解密等。 但是菜作好了,如何告訴服務員呢? ==回調==

事件驅動

談談瀏覽器中的Event Loop

image

  • 渲染引擎

渲染引擎內部是多線程的,包括 UI 線程和 JS 線程。注意 UI 線程和 JS 線程是互斥的,由於 JS 運行結果會影響到 UI 線程的結果。 UI 更新會被保存在任務隊列中等 JS 線程空閒時候當即被執行。

  • JS 單線程(主線程)

JS 在最初爲何被設計成了單線程,而不是多線程呢?若是多個線程同時操做 DOM 那豈不是會很混亂?

  • 其餘線程

    • 瀏覽器事件觸發線程(用來控制事件循環、存放seTimeout、瀏覽器事件、ajax回調)
    • 定時觸發器線程(setTimeout)
    • 異步 HTTP 請求線程(ajax請求線程)

瀏覽器中的 Event Loop

來個經典的圖

image

  • 全部同步任務都在主線程上執行,造成一個執行棧
  • 主線程以外,還存在一個任務隊列,只要異步任務有了運行結果,就在任務隊列中放一個事件
  • 一旦執行棧中的全部同步任務執行完畢,系統就會讀取任務隊列,將任務隊列中的事件放到執行棧中依次執行
  • 主線程從任務隊列中讀取事件,這個過程是循環不斷的

棧內存 / 堆內存

javascript 中的變量分爲基本類型和引用類型。基本類型是保存在棧內存中,引用類型則指的是保存在堆內存中的對象

image

任務隊列

  • 任務隊列 先進先出

宏觀任務(MacroTask):

setTimeout
setInterval
setImmediate(只兼容IE)
MessageChannel
requestAnimationFrame
I/O
UI rendering 
複製代碼

微觀任務(MicroTask):

process.nextTick
Promise
Object.observe(已廢棄)
MutationObserver
複製代碼
  • 棧 先進後出

好比: 函數的執行棧,做用域的釋放順序。

放進去的順序: 全局做用域 <= one <= two <= three

函數 three 沒有執行完,函數 one 是不會被釋放的。函數銷燬的順序則是 three => two => one => 全局

image

function one () {
  let a = 1;
  two();
  function two() {
    console.log(a);
    let b = 2;
    function three () {
      debugger;
      console.log(b);
    }
    three();
  }
}

one();
複製代碼

斷點調試下以下:進入的順序和出去的順序是相反的。

image

例子-1:

// 棧中的代碼執行完畢後,會調用隊列中的代碼,此過程不挺的循環
// 當 1000 毫秒到達的時候 setTimeout 纔會被放到任務隊列裏去
console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000)

setTimeout(() => {
  console.log(3);
}, 500)

// 1 3 2
複製代碼

例子-2:

console.log(1);
setTimeout(() => {
  console.log(2);
}, 1000)
while(true) {}
setTimeout(() => {
  console.log(3);
}, 500)
複製代碼

此時不會輸出 2 和 3。是由於 while 是個死循環。當時間到達時,要看棧中是否已經執行完了,若是沒有執行完,就不會調用隊列中的內容

例子-3:

console.log('global')
for (var i = 1;i <= 5;i ++) {
  setTimeout(function() {
    console.log('setTimeout1:', i)
  },i*1000)
  console.log(i)
}

new Promise(function (resolve) {
  console.log('promise1')
  resolve()
 }).then(function () {
  console.log('then1')
})

setTimeout(function () {
  console.log('timeout2')
  new Promise(function (resolve) {
    console.log('timeout2_promise')
    resolve()
  }).then(function () {
    console.log('timeout2_then')
  })
}, 1000)
// 輸出結果
// global
// 1 
// 2
// 3
// 4
// 5
// promise1
// then1
// setTimeout1: 6
// timeout2
// timeout2_pormise 
// timeout2_then 
// setTimeout1: 6 
// setTimeout1: 6 
// setTimeout1: 6 
// setTimeout1: 6
// setTimeout1: 6 

複製代碼

先執行主線程中的任務輸出 global 和 for 循環中的 i。setTimeout 屬於宏觀任務,時間到了會放到宏任務隊列中,setTimeout1 會根據 i * 1000 依次放入到宏任務隊列中。Promise 構造函數中的執行器屬於同步任務,會先輸出 promise1, 調用 resolve 後改變了 Promise 狀態,調用 then 方法會將任務放入微任務隊列中。此時 setTimeout2 時間到會被放入宏任務隊列中。timeout2_promise 也會根據promise1的執行過程進入到微任務隊列。

每次 Event Loop 觸發執行的過程是:

A> 執行主線程中的任務,調用棧爲空

B> 取出==全部== micro-task 任務隊列 => 執行

C> 取出==一個== macro-task 任務 => 執行

D> 取出==全部== micro-task 任務隊列 => 執行

E> 重複 C 和 D

node 系統中的 Event Loop

image

  • js 代碼會交給 V8 引擎進行處理
  • 代碼中用到的 node api 會交給 libuv 庫處理
  • libuv 經過阻塞 i/o 和多線程實現異步 io
  • 經過事件驅動方式,將結果放到事件隊列中,最終交給個人應用
相關文章
相關標籤/搜索