Node.js 中的異步編程

由于 JavaScript 是單線(xiàn)程運(yùn)行的,如果單線(xiàn)程的所有程序都是同步執(zhí)行的,那么一旦某段程序調(diào)用堵塞,整個(gè)線(xiàn)程就掛起了。所以 JavaScript 天生是異步的。


Node.js 使用的主要編程語(yǔ)言是 JavaScript,采用異步編程,其主要特點(diǎn)如下:

單線(xiàn)程相比多線(xiàn)程而已,最大的劣勢(shì)就是無(wú)法充分使用利用多核 CPU。

1.但是單線(xiàn)程也避免了多線(xiàn)程中的存在的一些問(wèn)題:線(xiàn)程的創(chuàng)建和上下文切換開(kāi)銷(xiāo)大以及多線(xiàn)程經(jīng)常面臨鎖,狀態(tài)同步問(wèn)題。

2.而采用異步 I/O 編程,遠(yuǎn)離線(xiàn)程被外部調(diào)用所阻塞,可以充分使用單核CPU。

3.為了彌補(bǔ)單線(xiàn)程無(wú)法使用多核 CPU 的缺點(diǎn),Node 提供了子工作進(jìn)程的方式去高效使用 CPU。

4.最后我們?cè)诓渴鸬姆?wù)的時(shí)候,同一臺(tái)機(jī)器可以部署多個(gè)實(shí)例,可以充分利用 CPU。


Node.js 中異步原理


阻塞IO/非阻塞IO


什么叫 IO 呢?一般是指除了 CPU 之外的外部設(shè)備的任務(wù)都叫 I/O 操作。最常見(jiàn)的 I/O 操作類(lèi)型就是文件操作和 TCP/UDP 網(wǎng)絡(luò)操作

操作系統(tǒng)對(duì)計(jì)算機(jī)進(jìn)行了抽象,將所有輸入輸出設(shè)備定義成文件,內(nèi)核在進(jìn)行 IO 操作時(shí),通過(guò)文件描述符進(jìn)行管理,應(yīng)用程序在進(jìn)行 IO 調(diào)用時(shí),根據(jù)文件描述符去實(shí)現(xiàn) IO 數(shù)據(jù)的讀取,非阻塞 IO 和阻塞 IO 的區(qū)別在于,阻塞 IO 需要完成整個(gè)文件的讀取過(guò)程,而非阻塞 IO 可以不帶數(shù)據(jù)直接返回,然后再根據(jù)文件描述符區(qū)輪詢(xún)查詢(xún)返回?cái)?shù)據(jù)。而 Node 正式利用非阻塞 IO 實(shí)現(xiàn)異步編程的。


異步 IO 原理


異步 IO 是指應(yīng)用層以異步的方式去讀取非阻塞 IO 的方式,只有非阻塞 IO 才能執(zhí)行異步操作。Node 底層采用線(xiàn)程池的原理管理異步 IO,所以我們通常所的 單線(xiàn)程是指 Node 中 JavaScript 的執(zhí)行是單線(xiàn)程的,但 Node 本身是多線(xiàn)程的。Node.js 中異步 IO 是通過(guò)事件循環(huán)的方式實(shí)現(xiàn)的,異步 IO 事件主要來(lái)源于網(wǎng)絡(luò)請(qǐng)求和文件 IO。事件循環(huán)主要由以下幾個(gè)部分實(shí)現(xiàn):

1.事件循環(huán):Node 啟動(dòng)進(jìn)程后,便會(huì)創(chuàng)建一個(gè)類(lèi)似 while 的循環(huán),每次循環(huán)我們稱(chēng)為一個(gè) tick,在循環(huán)的過(guò)程中,每次都要查看是否有事件需要處理,如果有,則取出處理,如果沒(méi)有事件需要處理則直接退出。

2.觀察者:每個(gè)事件循環(huán)中,會(huì)有一個(gè)多個(gè)觀察者存在,事件循環(huán)的過(guò)程就是不斷詢(xún)問(wèn)觀察者有沒(méi)有需要處理的事件的過(guò)程。

3.請(qǐng)求對(duì)象:請(qǐng)求對(duì)象是異步 IO 的中間產(chǎn)物,所有狀態(tài)都保存在這個(gè)請(qǐng)求對(duì)象中,包括送入線(xiàn)程池以及 IO 操作完畢的回調(diào)處理,告訴觀察者。

4.線(xiàn)程池:多個(gè)線(xiàn)程池按照一定的算法并發(fā)執(zhí)行請(qǐng)求對(duì)象,執(zhí)行完請(qǐng)求對(duì)象通知 IOCP 調(diào)用完成,通知觀察者,放入觀察者列表

而異步 IO 的事件調(diào)用模型在不同的操作系統(tǒng)上實(shí)現(xiàn)不一樣,Linux 系統(tǒng)中是 epoll, 在 BSD 系統(tǒng)(MacOS)中是 kqueue, 在 Solaris 系統(tǒng)中是 event ports, 在 Windows 系統(tǒng)中是 IOCP(Input Output Completion Port)。但是 Node.js 使用 libuv 做統(tǒng)一封裝,兼容所有平臺(tái)的異步事件邏輯。



異步編程的分類(lèi)

解決異步問(wèn)題方法大致包括:直接回調(diào)、pub/sub模式(事件模式)、異步庫(kù)控制庫(kù)(例如async、when)、promise、Generator等。

回調(diào)函數(shù)

????? 回調(diào)函數(shù)是常用的解決異步的方法,經(jīng)常接觸和使用到,易于理解,并且在庫(kù)或函數(shù)中非常容易實(shí)現(xiàn)。這種也是大家接使用異步編程經(jīng)常使用到的方法。

????? 但是回調(diào)函數(shù)的方式存在如下的問(wèn)題:

???? 1. 可能形成萬(wàn)惡的嵌套金字塔,代碼不易閱讀;

???? 2. 只能對(duì)應(yīng)一個(gè)回調(diào)函數(shù),在很多場(chǎng)景中成為一個(gè)限制。

pub/sub模式(事件)

???? 該模式也稱(chēng)為事件模式,是回調(diào)函數(shù)的事件化,在jQuery等類(lèi)庫(kù)中非常常見(jiàn)。

???? 事件發(fā)布訂閱者模式本身并無(wú)同步與異步調(diào)用的問(wèn)題,但是在node中,emit調(diào)用多半是伴隨事件循環(huán)而異步觸發(fā)的。該模式常用來(lái)解耦業(yè)務(wù)邏輯,事件發(fā)布者無(wú)須關(guān)注注冊(cè)的回調(diào)函數(shù),也不用關(guān)注回調(diào)函數(shù)的個(gè)數(shù),數(shù)據(jù)通過(guò)消息的方式可以很靈活的傳遞。

???? 該模式的好處是:1. 便于理解;2. 不再局限于一個(gè)回調(diào)函數(shù)。

???? 不好的地方時(shí):1. 需要借助類(lèi)庫(kù); 2.事件與回調(diào)函數(shù)的順序很重要

代碼如下:

var img = document.querySelect(#id);

img.addEventListener('load', function() {

// 圖片加載完成

......

});

img.addEventListener('error', function() {

// 出問(wèn)題了

......

});

  上述代碼存在兩個(gè)問(wèn)題:

????? a. img實(shí)際已經(jīng)加載完成,此時(shí)才綁定load回調(diào)函數(shù),結(jié)果回調(diào)不會(huì)執(zhí)行,但依然希望執(zhí)行該對(duì)應(yīng)回調(diào)函數(shù)。

代碼如下:

var img = document.querySelect(#id);

function load() {

...

}

if(img.complete) {

load();

} else {

img.addEventListener('load', load);

}

img.addEventListener('error', function() {

// 出問(wèn)題了

......

});

   b. 無(wú)法很好處理存在異常

????? 結(jié)論:事件機(jī)制最適合處理同一個(gè)對(duì)象上反復(fù)發(fā)生的事情,不需要考慮當(dāng)綁定回調(diào)函數(shù)之前事件發(fā)生的情況。

異步控制庫(kù)

????? 目前的異步庫(kù)主要有Q、when.js、win.js、RSVP.js等。

????? 這些庫(kù)的特點(diǎn)是代碼是線(xiàn)性的,可以從上到下完成書(shū)寫(xiě),符合自然習(xí)慣。

????? 不好的地方也是風(fēng)格各異,不便于閱讀,增加學(xué)習(xí)成本。

Promise

???? Promise翻譯成中文為承諾,個(gè)人理解是異步完成之后,就會(huì)給外部一個(gè)結(jié)果(成功或失?。⒊兄Z結(jié)果不再發(fā)生改變。換句話(huà)就是Promise反應(yīng)了一個(gè)操作的最終返回結(jié)果值(A promise represents the eventual value returned from the single completion of an operation)。目前Promise已經(jīng)引入到ES6規(guī)范里面,Chrome、firefox等高級(jí)瀏覽器已經(jīng)在內(nèi)部實(shí)現(xiàn)了該原生方法,使用起來(lái)相當(dāng)方便。

?狀態(tài)

???? 包含三種狀態(tài):pending、fulfilled、rejected,三種狀態(tài)只能發(fā)生兩種轉(zhuǎn)換(從pending--->fulfilled、pending—>rejected),并且狀態(tài)的轉(zhuǎn)換僅能發(fā)生一次。

?then方法

? ?then方法用于指定異步事件完成之后的回調(diào)函數(shù)。

?? ????這個(gè)方法可以說(shuō)是Promise的靈魂方法,該方法讓Promise充滿(mǎn)了魔力。有如下幾個(gè)具體表現(xiàn):

? ? ? ? then方法返回Promise。這樣就實(shí)現(xiàn)了多個(gè)異步操作的串行操作。

????實(shí)現(xiàn)了多個(gè)不同異步庫(kù)之間的轉(zhuǎn)換。

? ??????? 在異步中存在一個(gè)叫thenable的對(duì)象,就是指具有then方法的對(duì)象,只要一個(gè)對(duì)象對(duì)象具有then方法,就可以對(duì)其進(jìn)行轉(zhuǎn)換

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容