node 的特點
- 主執行緒是單執行緒
- 非同步非阻塞(非阻塞I/O)
- 事件驅動
單執行緒
- 程式和執行緒
程式是作業系統分配資源和排程任務的基本單位,執行緒是建立在程式上的一次程式執行單位,一個程式上可以有多個執行緒。
- 什麼是單執行緒?
一個程式中只有一個執行緒,程式順序執行,前面的執行完成後才會執行後面的程式。
有點兒像點菜一樣, 顧客來了。服務員接單,服務員接完單後馬上告訴廚房去做菜吧,此時服務員不會等待菜做好而是馬上會被釋放出來繼續服務下一個客戶。 一直都是一個服務員在工作,等菜做好了,服務員在回去取菜給客戶吃。
單執行緒特點是節約記憶體,並且不需要再切換執行上下文,而且單執行緒不需要考慮鎖的問題。
如下圖
非同步非阻塞
- 同步和非同步
同步和非同步關注的是訊息通知機制,指代的是被呼叫方
同步就是發出呼叫後,沒有得到結果前,該呼叫不返回,一旦呼叫返回,就得到返回值
當一個非同步過程呼叫發出後,呼叫者不會立刻得到結果,而是呼叫發出後,被呼叫者通過狀態、通知或者回撥函式處理這個呼叫。
- 阻塞和非阻塞
阻塞和非阻塞關注的是程式在等待呼叫結果(訊息、返回值)的狀態,針對的是呼叫者
阻塞呼叫是指呼叫結果返回之前,當前執行緒會被掛起。呼叫執行緒只有在得到結果後才會返回
非阻塞呼叫指在不能立刻得到結果前,該呼叫不會阻塞當前執行緒
a> 同步阻塞
呼叫者:我喜歡你
被呼叫者:我也喜歡你
b> 非同步阻塞
呼叫者: 我喜歡你
被呼叫者:我和我媽商量下。回頭回覆你
呼叫者: 不結束通話電話,我等你回覆
c> 非同步非阻塞
呼叫者: 我喜歡你
被呼叫者: 我和我媽商量下。回頭回覆你
呼叫者: 結束通話電話,不等你了。聯絡了另外一個妹子
d> 同步非阻塞
呼叫者:給 A 打電話 我喜歡你
被呼叫者(A):正準備回覆
呼叫者:不結束通話電話 A 轉身又給 B 打電話 我喜歡你
複製程式碼
-
I/O 操作
-
訪問伺服器的靜態資源
-
讀取資料,讀取檔案
-
node 在處理高併發, I/O密集場景有明顯優勢。高併發是指在同一時間併發訪問伺服器,I/O密集知道是檔案操作、網路操作、資料庫。相對的有 CPU 密集,CPU 密集值的是邏輯處理運算、壓縮、解壓、加密、解密等。 可是菜做好了,如何告訴服務員呢? ==回撥==
事件驅動
談談瀏覽器中的Event Loop
- 渲染引擎
渲染引擎內部是多執行緒的,包括 UI 執行緒和 JS 執行緒。注意 UI 執行緒和 JS 執行緒是互斥的,因為 JS 執行結果會影響到 UI 執行緒的結果。 UI 更新會被儲存在任務佇列中等 JS 執行緒空閒時候立即被執行。
- JS 單執行緒(主執行緒)
JS 在最初為什麼被設計成了單執行緒,而不是多執行緒呢?如果多個執行緒同時操作 DOM 那豈不是會很混亂?
-
其他執行緒
- 瀏覽器事件觸發執行緒(用來控制事件迴圈、存放seTimeout、瀏覽器事件、ajax回撥)
- 定時觸發器執行緒(setTimeout)
- 非同步 HTTP 請求執行緒(ajax請求執行緒)
瀏覽器中的 Event Loop
來個經典的圖
- 所有同步任務都在主執行緒上執行,形成一個執行棧
- 主執行緒之外,還存在一個任務佇列,只要非同步任務有了執行結果,就在任務佇列中放一個事件
- 一旦執行棧中的所有同步任務執行完畢,系統就會讀取任務佇列,將任務佇列中的事件放到執行棧中依次執行
- 主執行緒從任務佇列中讀取事件,這個過程是迴圈不斷的
棧記憶體 / 堆記憶體
javascript 中的變數分為基本型別和引用型別。基本型別是儲存在棧記憶體中,引用型別則指的是儲存在堆記憶體中的物件
任務佇列
- 任務佇列 先進先出
巨集觀任務(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 => 全域性
function one () {
let a = 1;
two();
function two() {
console.log(a);
let b = 2;
function three () {
debugger;
console.log(b);
}
three();
}
}
one();
複製程式碼
斷點除錯下如下:進入的順序和出去的順序是相反的。
例子-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
- js 程式碼會交給 V8 引擎進行處理
- 程式碼中用到的 node api 會交給 libuv 庫處理
- libuv 通過阻塞 i/o 和多執行緒實現非同步 io
- 通過事件驅動方式,將結果放到事件佇列中,最終交給我的應用