培育能力的事必須繼續不斷地去做,又必須隨時改善學習方法,提高學習效率,才會成功。 —— 葉聖陶
一、我們為什麼要使用node,它的好處是什麼?
Node的首要目標是提供一種簡單的,用於建立高效能伺服器的開發工具。還要解決web伺服器高併發的使用者請求。
解決高併發?
我們這裡來舉個例子,我們node和java相比,在同樣的請求下誰更佔優一點。看圖
- 當使用者請求量增高時,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全集,因為在服務端中不包含DOM
和BOM
,Node也提供了一些新的模組例如http,fs
模組等。Node.js 使用了事件驅動、非阻塞式 I/O
的模型,使其輕量又高效並且Node.js 的包管理器 npm
,是全球最大的開源庫生態系統。
總而言之,言而總之,它只是一個執行時,一個執行環境。
node特性
- 主執行緒是單執行緒(非同步),將後續的邏輯寫成函式,傳入到當前執行的函式中,當執行的函式得到了結果後,執行傳入的函式
(回撥函式)
。 - 五個人同時吃一碗飯(非同步)。
- 阻塞不能非同步(現在假定資料庫是廚師,服務員是node,顧客是請求,一般是廚師做菜讓一個服務員遞給多個使用者,如果廚師邀請服務員聊天,就會導致阻塞,並且是針對核心說的)。
- i/o操作,讀寫操作,非同步讀寫(能用非同步絕不用同步)
非阻塞式i/o
,即可以非同步讀寫。 - event-driven
事件驅動
(釋出訂閱)。
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,現在大家已經知道堆,棧和佇列的關係,現在我們來看一張圖。
我分析一下這張圖
- 我們的同步任務在主執行緒上執行會形成一個執行棧
- 如果碰到非同步任務,比如
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
四、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 loop
在check佇列 - 然後
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
- 根據node事件環的規則,我們會執行完所有的事件,即取出timers佇列中的
- 假如這裡
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
比上面要多切換一次
- 此時
所以有兩種答案
setImmediate1,setTimeout2,setTimeout1,nextTick1,setImmediate2
setImmediate1,setTimeout2,nextTick1,setImmediate2,setTimeout1
這裡的圖只參考了第一種情況,另一種情況也類似
五、node的同步、非同步,阻塞、非阻塞
- 同步:即為呼叫者等待被呼叫者這個過程,如果被呼叫者一直不反回結果,呼叫者就會一直等待,這就是同步,同步有返回值
- 非同步:即為呼叫者不等待被呼叫者是否返回,被呼叫者執行完了就會通過狀態、通知或者回撥函式給呼叫者,非同步沒有返回值
- 阻塞:指代當前執行緒在結果返回之前會被掛起,不會繼續執行下去
- 非阻塞: 即當前執行緒不管你返回什麼,都會繼續往下執行
有些人可能會搞亂他們之間的關係,同步、非同步
是被呼叫者的狀態,阻塞、非阻塞
是呼叫者的狀態、訊息
接下來我們來看看他們的組合會是怎麼樣的
組合 | 意義 |
---|---|
同步阻塞 | 這就相當於我去飯店吃飯,我需要在廚房等待菜燒好了,才能吃。我是呼叫者我需要等待上菜於是被阻塞,菜是被呼叫者做好直接給我是同步 |
非同步阻塞 | 我去飯店吃飯,我需要等待菜燒好了才能吃,但是廚師有事,希望之後處理完事能做好之後通知我去拿,我作為呼叫者等待就是阻塞的,而菜作為被呼叫者是做完之後通知我的,所以是非同步的,這種方式一般沒用。 |
同步非阻塞 | 我去飯店吃飯,先叫了碗熱菜,在廚房等廚師做菜,但我很餓,就開始吃廚房冷菜,我是呼叫者我沒等熱菜好就開始吃冷菜,是非阻塞的,菜作為被呼叫者做好直接給我是同步的,這種方式一般也沒人用 |
非同步非阻塞 | 我去飯店吃飯。叫了碗熱菜,廚師在做菜,但我很餓,先吃冷菜,廚師做好了通知我去拿,我是呼叫者我不會等熱菜燒好了再吃冷菜,是非阻塞的,菜作為被呼叫者通知我拿是非同步的 |
結尾
希望大家看了本篇文章都有收穫,這樣出去面試的時候就不會這樣
而是這樣。好了,最後希望大家世界盃都能夠逢賭必贏,自己喜歡的球隊也能夠殺進決賽。