整理幾個(gè)關(guān)鍵節(jié)點(diǎn)深入理解nodejs
前言
本文是個(gè)人在實(shí)際開(kāi)發(fā)和學(xué)習(xí)中對(duì)nodejs的一些理解,現(xiàn)整理出來(lái)方便日后查閱,如果能給您啟發(fā)將不勝榮幸。
非阻塞I/O
I/O:即 Input / Output,一個(gè)系統(tǒng)的輸入和輸出。
一個(gè)系統(tǒng)可以理解為一個(gè)個(gè)體,比如說(shuō)一個(gè)人,你說(shuō)話(huà)就是輸出,你聽(tīng)就是輸入。
阻塞 I/O 與非阻塞 I/O 的區(qū)別就在于系統(tǒng)接收輸入再到輸出期間,能不能接收其他輸入。
下面以?xún)蓚€(gè)例子來(lái)說(shuō)明什么是阻塞 I/O 和非阻塞 I/O:
打飯

首先我們要確定一個(gè)系統(tǒng)的范圍,在這個(gè)例子中食堂阿姨和餐廳的服務(wù)生看成是一個(gè)系統(tǒng),輸入就是點(diǎn)菜,輸出就是端菜。
那么在點(diǎn)菜和端菜之間能不能接受其他人的點(diǎn)菜,就可以判斷是阻塞I/O還是非阻塞I/O。
對(duì)于食堂阿姨,他在點(diǎn)菜的時(shí)候,是不能幫其他同學(xué)點(diǎn)菜的,只有這個(gè)同學(xué)點(diǎn)完菜端菜走了之后,才能接受下一個(gè)同學(xué)的點(diǎn)菜,所以食堂阿姨是阻塞I/O。
對(duì)于餐廳服務(wù)員,他可以在點(diǎn)完菜以后,這個(gè)客人端菜之前是可以服務(wù)下一位客人的,所以服務(wù)員是非阻塞I/O。
做家務(wù)

在洗衣服的時(shí)候,是不需要等著洗衣機(jī)旁邊的,這個(gè)時(shí)候可以去掃地和整理書(shū)桌,當(dāng)整理完書(shū)桌后衣服也洗好了,這個(gè)時(shí)候去晾衣服,那么總共只需要25分鐘。
洗衣服其實(shí)就是一個(gè)非阻塞I/O,在把衣服扔進(jìn)洗衣機(jī)和洗完衣服期間,你是可以干其他事情的。
非阻塞I/O之所以能提升性能,是因?yàn)樗梢园巡槐匾牡却o節(jié)省掉。
理解非阻塞I/O的要點(diǎn)在于:
- 確定一個(gè)進(jìn)行I/O的系統(tǒng)邊界。這非常關(guān)鍵,如果把系統(tǒng)擴(kuò)大,上面餐廳的例子,如果把系統(tǒng)擴(kuò)大到整個(gè)餐廳,那么廚師肯定是一個(gè)阻塞 I/O。
- 在 I/O 過(guò)程中,能不能進(jìn)行其他 I/O。
nodejs的非阻塞 I/O
nodejs的非阻塞 I/O 是怎么體現(xiàn)的呢?前面說(shuō)過(guò)理解非阻塞 I/O 的一個(gè)重要點(diǎn)是先確定一個(gè)系統(tǒng)邊界,nodejs的系統(tǒng)邊界就是主線(xiàn)程。
如果下面的架構(gòu)圖按照線(xiàn)程的維護(hù)劃分,左邊虛線(xiàn)部分是nodejs線(xiàn)程,右邊虛線(xiàn)部分是c++線(xiàn)程。

現(xiàn)在 nodejs 線(xiàn)程需要去查詢(xún)數(shù)據(jù)庫(kù),這是一個(gè)典型的 I/O 操作,它不會(huì)等待 I/O 的結(jié)果,而且繼續(xù)處理其他的操作,它會(huì)把大量的計(jì)算能力分發(fā)到其他的c++線(xiàn)程去計(jì)算。
等到結(jié)果出來(lái)后返回給nodejs線(xiàn)程,在獲得結(jié)果之前nodejs 線(xiàn)程還能進(jìn)行其他的I/O操作,所以是非阻塞的。
nodejs 線(xiàn)程 相當(dāng)于左邊部分是服務(wù)員,c++ 線(xiàn)程是廚師。
所以,node的非阻塞I/O是通過(guò)調(diào)用c++的worker threads來(lái)完成的。
那當(dāng) c++ 線(xiàn)程獲取結(jié)果后怎么通知 nodejs 線(xiàn)程呢?答案是事件驅(qū)動(dòng)。
事件驅(qū)動(dòng)
阻塞:I/O時(shí)進(jìn)程休眠,等待I/O完成后進(jìn)行下一步;
非阻塞:I/O時(shí)函數(shù)立即返回,進(jìn)程不等待I/O完成。
那怎么知道返回的結(jié)果,就需要用到事件驅(qū)動(dòng)。
所謂事件驅(qū)動(dòng)可以理解為跟前端點(diǎn)擊事件一樣,我首先寫(xiě)一個(gè)點(diǎn)擊事件,但是我不知道什么時(shí)候觸發(fā),只有觸發(fā)的時(shí)候就去讓主線(xiàn)程執(zhí)行事件驅(qū)動(dòng)函數(shù)。
這種模式也是一種觀(guān)察者模式,就是我首先先監(jiān)聽(tīng)這個(gè)事件,等觸發(fā)時(shí)我就去執(zhí)行。
那怎么實(shí)現(xiàn)事件驅(qū)動(dòng)呢?答案是異步編程。
異步編程
上面說(shuō)過(guò)nodejs有大量的非阻塞I/O,那么非阻塞I/O的結(jié)果是需要通過(guò)回調(diào)函數(shù)來(lái)獲取的,這種通過(guò)回調(diào)函數(shù)的方式,就是異步編程。比如下面的代碼是通過(guò)回調(diào)函數(shù)獲取結(jié)果的:
glob(__dirname+'/**/*', (err, res) => {
result = res
console.log('get result')
})
回調(diào)函數(shù)格式規(guī)范
nodejs的回調(diào)函數(shù)第一個(gè)參數(shù)是error,后面的參數(shù)才是結(jié)果。為什么要這么做呢?
try {
interview(function () {
console.log('smile')
})
} catch(err) {
console.log('cry', err)
}
function interview(callback) {
setTimeout(() => {
if(Math.random() < 0.1) {
callback('success')
} else {
throw new Error('fail')
}
}, 500)
}執(zhí)行之后,沒(méi)有被捕獲,錯(cuò)誤被扔到了全局,導(dǎo)致整個(gè)nodejs程序崩潰了。

沒(méi)有被try catch捕獲是因?yàn)閟etTimeout重新開(kāi)啟了事件循環(huán),每開(kāi)啟一個(gè)事件循環(huán)就重新生一個(gè)調(diào)用棧context,try catch是屬于上一個(gè)事件循環(huán)的調(diào)用棧的,setTimeout的回調(diào)函數(shù)執(zhí)行的時(shí)候,調(diào)用棧都不一樣了,在這個(gè)新的調(diào)用棧中是沒(méi)有try catch,所以這個(gè)錯(cuò)誤被扔到全局,無(wú)法捕獲。具體可以參考這一篇文章JavaScript異步隊(duì)列進(jìn)行try catch時(shí)的問(wèn)題解決。
那么怎么辦呢?把錯(cuò)誤也作為一個(gè)參數(shù):
function interview(callback) {
setTimeout(() => {
if(Math.random() < 0.5) {
callback('success')
} else {
callback(new Error('fail'))
}
}, 500)
}
interview(function (res) {
if (res instanceof Error) {
console.log('cry')
return
}
console.log('smile')
})但是這樣就比較麻煩,在回調(diào)中還要判斷,所以就產(chǎn)生一種約定成熟的規(guī)定,第一個(gè)參數(shù)是err,如果不存在表示執(zhí)行成功。
function interview(callback) {
setTimeout(() => {
if(Math.random() < 0.5) {
callback(null, 'success')
} else {
callback(new Error('fail'))
}
}, 500)
}
interview(function (res) {
if (res) {
return
}
console.log('smile')
})異步流程控制
nodejs的回調(diào)寫(xiě)法,不僅會(huì)帶來(lái)回調(diào)地域,還會(huì)帶來(lái)異步流程控制的問(wèn)題。
異步流程控制主要是指當(dāng)并發(fā)的時(shí)候,怎么來(lái)處理并發(fā)的邏輯。還是上面的例子,如果你同事面試兩家公司,只有當(dāng)成功面試兩家的時(shí)候,才可以不面試第三家,那么怎么寫(xiě)這個(gè)邏輯呢?需要全局頂一個(gè)一個(gè)變量count:
var count = 0
interview((err) => {
if (err) {
return
}
count++
if (count >= 2) {
// 處理邏輯
}
})
interview((err) => {
if (err) {
return
}
count++
if (count >= 2) {
// 處理邏輯
}
})像上面這種寫(xiě)法就非常麻煩,且難看。所以,后來(lái)就出現(xiàn)了promise,async/await的寫(xiě)法。
promise
當(dāng)前事件循環(huán)得不到的結(jié)果,但未來(lái)的事件循環(huán)會(huì)給你結(jié)果。很像一個(gè)渣男說(shuō)的話(huà)。
promise不僅是一個(gè)渣男,還是一個(gè)狀態(tài)機(jī):
- pending
- fulfilled/resolved
- rejectd
const pro = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('2')
}, 200)
})
console.log(pro) // 打?。篜romise { <pending> }then & .catch
- resolved 狀態(tài)的 promise 會(huì)調(diào)用后面的第一個(gè) then
- rejected 狀態(tài)的 promise 會(huì)調(diào)用后面的第一個(gè) catch
- 任何一個(gè) reject 狀態(tài)且后面沒(méi)有 .catch 的 promise,都會(huì)造成瀏覽器或者 node 環(huán)境的全局錯(cuò)誤。uncaught 表示未捕獲的錯(cuò)誤。

執(zhí)行then或者catch會(huì)返回一個(gè)新的promise,該promise最終狀態(tài)根據(jù)then和catch的回調(diào)函數(shù)的執(zhí)行結(jié)果決定:
- 如果回調(diào)函數(shù)始終是throw new Error,該promise是rejected狀態(tài)
- 如果回調(diào)函數(shù)始終是return,該promise是resolved狀態(tài)
- 但如果回調(diào)函數(shù)始終是return一個(gè)promise,該promise會(huì)和回調(diào)函數(shù)return的promise狀態(tài)保持一致。
function interview() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('success')
} else {
reject(new Error('fail'))
}
})
})
}
var promise = interview()
var promise1 = promise.then(() => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve('accept')
}, 400)
})
})promise1的狀態(tài)是由return里面的promise的狀態(tài)決定的,也就是return里面的promise執(zhí)行完后的狀態(tài)就是promise1的狀態(tài)。這樣有什么好處呢?這樣可以解決回調(diào)地獄的問(wèn)題。
var promise = interview()
.then(() => {
return interview()
})
.then(() => {
return interview()
})
.then(() => {
return interview()
})
.catch(e => {
console.log(e)
})then如果返回的promise的狀態(tài)是rejected,那么會(huì)調(diào)用后面第一個(gè)catch,后面的then就不會(huì)在調(diào)用了。記?。簉ejected調(diào)用后面的第一個(gè)catch,resolved調(diào)用后面的第一個(gè)then。
promise解決異步流程控制
如果promise僅僅是為了解決地獄回調(diào),太小看promise了,promise最主要的作用是解決異步流程控制問(wèn)題。下面如果要同時(shí)面試兩家公司:
function interview() {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (Math.random() > 0.5) {
resolve('success')
} else {
reject(new Error('fail'))
}
})
})
}
promise
.all([interview(), interview()])
.then(() => {
console.log('smile')
})
// 如果有一家公司rejected,就catch
.catch(() => {
console.log('cry')
})async/await
sync/await到底是什么:
console.log(async function() {
return 4
})
console.log(function() {
return new Promise((resolve, reject) => {
resolve(4)
})
})打印的結(jié)果一樣,也就是async/await是promse的語(yǔ)法糖而已。
我們知道try catch捕獲錯(cuò)誤是依賴(lài)調(diào)用棧的,只能捕獲到調(diào)用棧以上的錯(cuò)誤。但是如果使用await后能捕捉到調(diào)用棧所有函數(shù)的錯(cuò)誤。即便這個(gè)錯(cuò)誤是在另一個(gè)事件循環(huán)的調(diào)用棧拋出的,比如setTimeout。
改造面試代碼,可以看到代碼精簡(jiǎn)了很多。
try {
await interview(1)
await interview(2)
await interview(2)
} catch(e => {
console.log(e)
})如果是并行任務(wù)呢?
await Promise.all([interview(1), interview(2)])
事件循環(huán)
因?yàn)閚odejs的非阻塞 I/0, 所以需要利用事件驅(qū)動(dòng)的方式獲取 I/O 的結(jié)果,實(shí)現(xiàn)事件驅(qū)動(dòng)拿到結(jié)果必須使用異步編程,比如回調(diào)函數(shù)。那么如何來(lái)有序的執(zhí)行這些回調(diào)函數(shù)來(lái)獲取結(jié)果呢?那就需要使用事件循環(huán)。
事件循環(huán)是實(shí)現(xiàn) nodejs 非阻塞 I/O 功能的關(guān)鍵基礎(chǔ),非阻塞I/O和事件循環(huán)都是屬于 libuv 這個(gè)c++庫(kù)提供的能力。

代碼演示:
const eventloop = {
queue: [],
loop() {
while(this.queue.length) {
const callback = this.queue.shift()
callback()
}
setTimeout(this.loop.bind(this), 50)
},
add(callback) {
this.queue.push(callback)
}
}
eventloop.loop()
setTimeout(() => {
eventloop.add(() => {
console.log('1')
})
}, 500)
setTimeout(() => {
eventloop.add(() => {
console.log('2')
})
}, 800)setTimeout(this.loop.bind(this), 50)保證了50ms就會(huì)去看隊(duì)列中是否有回調(diào),如果有就去執(zhí)行。這樣就形成了一個(gè)事件循環(huán)。
當(dāng)然實(shí)際的事件要復(fù)雜的多,隊(duì)列也不止一個(gè),比如有一個(gè)文件操作對(duì)列,一個(gè)時(shí)間對(duì)列。
const eventloop = {
queue: [],
fsQueue: [],
timerQueue: [],
loop() {
while(this.queue.length) {
const callback = this.queue.shift()
callback()
}
this.fsQueue.forEach(callback => {
if (done) {
callback()
}
})
setTimeout(this.loop.bind(this), 50)
},
add(callback) {
this.queue.push(callback)
}
}總結(jié)
首先我們弄清楚了什么是非阻塞I/O,即遇到I/O立刻跳過(guò)執(zhí)行后面的任務(wù),不會(huì)等待I/O的結(jié)果。當(dāng)I/O處理好了之后就會(huì)調(diào)用我們注冊(cè)的事件處理函數(shù),這就叫事件驅(qū)動(dòng)。實(shí)現(xiàn)事件驅(qū)動(dòng)就必須要用異步編程,異步編程是nodejs中最重要的環(huán)節(jié),它從回調(diào)函數(shù)到promise,最后到async/await(使用同步的方法寫(xiě)異步邏輯)。
到此這篇關(guān)于整理幾個(gè)關(guān)鍵節(jié)點(diǎn)深入理解nodejs的文章就介紹到這了,更多相關(guān)深入理解nodejs內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
nodejs中簡(jiǎn)單實(shí)現(xiàn)Javascript Promise機(jī)制的實(shí)例
這篇文章主要介紹了nodejs中簡(jiǎn)單實(shí)現(xiàn)Javascript Promise機(jī)制的實(shí)例,本文在nodejs中簡(jiǎn)單實(shí)現(xiàn)一個(gè)promise/A 規(guī)范,需要的朋友可以參考下2014-12-12
Nodejs 和 Electron ubuntu下快速安裝過(guò)程
本文較為詳細(xì)的給大家介紹了Nodejs 和 Electron ubuntu下快速安裝過(guò)程,非常不錯(cuò),具有一定的參考借鑒價(jià)值,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05
nodejs使用redis作為緩存介質(zhì)實(shí)現(xiàn)的封裝緩存類(lèi)示例
這篇文章主要介紹了nodejs使用redis作為緩存介質(zhì)實(shí)現(xiàn)的封裝緩存類(lèi),涉及nodejs操作redis進(jìn)行緩存設(shè)置相關(guān)操作技巧,需要的朋友可以參考下2018-02-02
Node.js API詳解之 assert模塊用法實(shí)例分析
這篇文章主要介紹了Node.js API詳解之 assert模塊用法,結(jié)合實(shí)例形式分析了Node.js API中assert模塊基本函數(shù)、功能、用法及操作注意事項(xiàng),需要的朋友可以參考下2020-05-05
nodejs實(shí)現(xiàn)獲取某寶商品分類(lèi)
這篇文章主要介紹了nodejs實(shí)現(xiàn)獲取某寶商品分類(lèi),十分的簡(jiǎn)單實(shí)用,進(jìn)入后臺(tái)直接打開(kāi)控制臺(tái),把代碼粘進(jìn)去運(yùn)行就OK了,有需要的小伙伴可以參考下。2015-05-05
Node.js實(shí)現(xiàn)登陸注冊(cè)功能
這篇文章主要為大家詳細(xì)介紹了Node.js實(shí)現(xiàn)登陸注冊(cè)功能,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-08-08

