由于 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)換