Node異步和事件循環(huán)的深入講解
前言
Node 最初是為打造高性能的 Web 服務(wù)器而生,作為 JavaScript 的服務(wù)端運(yùn)行時(shí),具有事件驅(qū)動(dòng)、異步 I/O、單線程等特性?;谑录h(huán)的異步編程模型使 Node 具備處理高并發(fā)的能力,極大地提升服務(wù)器的性能,同時(shí),由于保持了 JavaScript 單線程的特點(diǎn),Node 不需要處理多線程下?tīng)顟B(tài)同步、死鎖等問(wèn)題,也沒(méi)有線程上下文切換所帶來(lái)的性能上的開(kāi)銷(xiāo)。基于這些特性,使 Node 具備高性能、高并發(fā)的先天優(yōu)勢(shì),并可基于它構(gòu)建各種高速、可伸縮網(wǎng)絡(luò)應(yīng)用平臺(tái)。
本文將深入 Node 異步和事件循環(huán)的底層實(shí)現(xiàn)和執(zhí)行機(jī)制,希望對(duì)你有所幫助。
為什么要異步?
Node 為什么要使用異步來(lái)作為核心編程模型呢?
前面說(shuō)過(guò),Node 最初是為打造高性能的 Web 服務(wù)器而生,假設(shè)業(yè)務(wù)場(chǎng)景中有幾組互不相關(guān)的任務(wù)要完成,現(xiàn)代主流的解決方式有以下兩種:
單線程串行依次執(zhí)行。
多線程并行完成。
單線程串行依次執(zhí)行,是一種同步的編程模型,它雖然比較符合程序員按順序思考的思維方式,易寫(xiě)出更順手的代碼,但由于是同步執(zhí)行 I/O,同一時(shí)刻只能處理單個(gè)請(qǐng)求,會(huì)導(dǎo)致服務(wù)器響應(yīng)速度較慢,無(wú)法在高并發(fā)的應(yīng)用場(chǎng)景下適用,且由于是阻塞 I/O,CPU 會(huì)一直等待 I/O 完成,無(wú)法做其他事情,使 CPU 的處理能力得不到充分利用,最終導(dǎo)致效率的低下,
而多線程的編程模型也會(huì)因?yàn)榫幊讨械臓顟B(tài)同步、死鎖等問(wèn)題讓開(kāi)發(fā)人員頭疼。盡管多線程在多核 CPU 上能夠有效提升 CPU 的利用率。
雖然單線程串行依次執(zhí)行和多線程并行完成的編程模型有其自身的優(yōu)勢(shì),但是在性能、開(kāi)發(fā)難度等方面也有不足之處。
除此之外,從響應(yīng)客戶(hù)端請(qǐng)求的速度出發(fā),如果客戶(hù)端同時(shí)獲取兩個(gè)資源,同步方式的響應(yīng)速度會(huì)是兩個(gè)資源的響應(yīng)速度之和,而異步方式的響應(yīng)速度會(huì)是兩者中最大的一個(gè),性能優(yōu)勢(shì)相比同步十分明顯。隨著應(yīng)用復(fù)雜度的增加,該場(chǎng)景會(huì)演變成同時(shí)響應(yīng) n 個(gè)請(qǐng)求,異步相比于同步的優(yōu)勢(shì)將會(huì)凸顯出來(lái)。
綜上所述,Node 給出了它的答案:利用單線程,遠(yuǎn)離多線程死鎖、狀態(tài)同步等問(wèn)題;利用異步 I/O,讓單線程遠(yuǎn)離阻塞,以更好地使用 CPU。這就是 Node 使用異步作為核心編程模型的原因。
此外,為了彌補(bǔ)單線程無(wú)法利用多核 CPU 的缺點(diǎn),Node 也提供了類(lèi)似瀏覽器中 Web Workers 的子進(jìn)程,該子進(jìn)程可以通過(guò)工作進(jìn)程高效地利用 CPU。
如何實(shí)現(xiàn)異步?
聊完了為什么要使用異步,那要如何實(shí)現(xiàn)異步呢?
我們通常所說(shuō)的異步操作總共有兩類(lèi):一是像文件 I/O、網(wǎng)絡(luò) I/O 這類(lèi)與 I/O 有關(guān)的操作;二是像 setTimeOut
、setInterval
這類(lèi)與 I/O 無(wú)關(guān)的操作。很明顯我們所討論的異步是指與 I/O 有關(guān)的操作,即異步 I/O。
異步 I/O 的提出是期望 I/O 的調(diào)用不會(huì)阻塞后續(xù)程序的執(zhí)行,將原有等待 I/O 完成的這段時(shí)間分配給其余需要的業(yè)務(wù)去執(zhí)行。要達(dá)到這個(gè)目的,就需要用到非阻塞 I/O。
阻塞 I/O 是 CPU 在發(fā)起 I/O 調(diào)用后,會(huì)一直阻塞,等待 I/O 完成。知道了阻塞 I/O,非阻塞 I/O 就很好理解了,CPU 在發(fā)起 I/O 調(diào)用后會(huì)立即返回,而不是阻塞等待,在 I/O 完成之前,CPU 可以處理其他事務(wù)。顯然,相比于阻塞 I/O,非阻塞 I/O 多于性能的提升是很明顯的。
那么,既然使用了非阻塞 I/O,CPU 在發(fā)起 I/O 調(diào)用后可以立即返回,那它是如何知道 I/O 完成的呢?答案是輪詢(xún)。
為了及時(shí)獲取 I/O 調(diào)用的狀態(tài),CPU 會(huì)不斷重復(fù)調(diào)用 I/O 操作來(lái)確認(rèn) I/O 是否已經(jīng)完成,這種重復(fù)調(diào)用判斷操作是否完成的技術(shù)就叫做輪詢(xún)。
顯然,輪詢(xún)會(huì)讓 CPU 不斷重復(fù)地執(zhí)行狀態(tài)判斷,是對(duì) CPU 資源的浪費(fèi)。并且,輪詢(xún)的間間隔很難控制,如果間隔太長(zhǎng),I/O 操作的完成得不到及時(shí)的響應(yīng),間接降低應(yīng)用程序的響應(yīng)速度;如果間隔太短,難免會(huì)讓 CPU 花在輪詢(xún)的耗時(shí)變長(zhǎng),降低 CPU 資源的利用率。
因此,輪詢(xún)雖然滿(mǎn)足了非阻塞 I/O 不會(huì)阻塞后續(xù)程序的執(zhí)行的要求,但是對(duì)于應(yīng)用程序而言,它仍然只能算是一種同步,因?yàn)閼?yīng)用程序仍然需要等待 I/O 完全返回,依舊花費(fèi)了很多時(shí)間來(lái)等待。
我們所期望的完美的異步 I/O,應(yīng)該是應(yīng)用程序發(fā)起非阻塞調(diào)用,無(wú)須通過(guò)輪詢(xún)的方式不斷查詢(xún) I/O 調(diào)用的狀態(tài),而是可以直接處理下一個(gè)任務(wù),在 I/O 完成后通過(guò)信號(hào)量或回調(diào)將數(shù)據(jù)傳遞給應(yīng)用程序即可。
如何實(shí)現(xiàn)這種異步 I/O 呢?答案是線程池。
雖然本文一直提到,Node 是單線程執(zhí)行的,但此處的單線程是指 JavaScript 代碼是執(zhí)行在單線程上的,對(duì)于 I/O 操作這類(lèi)與主業(yè)務(wù)邏輯無(wú)關(guān)的部分,通過(guò)運(yùn)行在其他線程的方式實(shí)現(xiàn),并不會(huì)影響或阻塞主線程的運(yùn)行,反而可以提高主線程的執(zhí)行效率,實(shí)現(xiàn)異步 I/O。
通過(guò)線程池,讓主線程僅進(jìn)行 I/O 的調(diào)用,讓其他多個(gè)線程進(jìn)行阻塞 I/O 或者非阻塞 I/O 加輪詢(xún)技術(shù)完成數(shù)據(jù)獲取,再通過(guò)線程之間的通信將 I/O 得到的數(shù)據(jù)進(jìn)行傳遞,這就輕松實(shí)現(xiàn)了異步 I/O:
主線程進(jìn)行 I/O 調(diào)用,而線程池進(jìn)行 I/O 操作,完成數(shù)據(jù)的獲取,然后通過(guò)線程之間的通信將數(shù)據(jù)傳遞給主線程,即可完成一次 I/O 的調(diào)用,主線程再利用回調(diào)函數(shù),將數(shù)據(jù)暴露給用戶(hù),用戶(hù)再利用這些數(shù)據(jù)來(lái)完成業(yè)務(wù)邏輯層面的操作,這就是 Node 中一次完整的異步 I/O 流程。而對(duì)于用戶(hù)來(lái)說(shuō),不必在意底層這些繁瑣的實(shí)現(xiàn)細(xì)節(jié),只需要調(diào)用 Node 封裝好的異步 API,并傳入處理業(yè)務(wù)邏輯的回調(diào)函數(shù)即可,如下所示:
const fs = require("fs"); fs.readFile('example.js', (data) => { // 進(jìn)行業(yè)務(wù)邏輯的處理 });
Node 的異步底層實(shí)現(xiàn)機(jī)制在不同平臺(tái)下有所不同:Windows 下主要通過(guò) IOCP 來(lái)向系統(tǒng)內(nèi)核發(fā)送 I/O 調(diào)用和從內(nèi)核獲取已完成的 I/O 操作,配以事件循環(huán),以此完成異步 I/O 的過(guò)程;Linux 下通過(guò) epoll 實(shí)現(xiàn)這個(gè)過(guò)程;FreeBSD下通過(guò) kqueue 實(shí)現(xiàn),Solaris 下通過(guò) Event ports 實(shí)現(xiàn)。線程池在 Windows 下由內(nèi)核(IOCP)直接提供,*nix
系列則由 libuv 自行實(shí)現(xiàn)。
由于 Windows 平臺(tái)和 *nix
平臺(tái)的差異,Node 提供了 libuv 作為抽象封裝層,使得所有平臺(tái)兼容性的判斷都由這一層來(lái)完成,保證上層的 Node 與下層的自定義線程池及 IOCP 之間各自獨(dú)立。Node 在編譯期間會(huì)判斷平臺(tái)條件,選擇性編譯 unix 目錄或是 win 目錄下的源文件到目標(biāo)程序中:
以上就是 Node 對(duì)異步的實(shí)現(xiàn)。
(線程池的大小可以通過(guò)環(huán)境變量 UV_THREADPOOL_SIZE
設(shè)置,默認(rèn)值為 4,用戶(hù)可結(jié)合實(shí)際情況來(lái)調(diào)整這個(gè)值的大小。)
那么問(wèn)題來(lái)了,在得到線程池傳遞過(guò)來(lái)的數(shù)據(jù)后,主線程是如何、何時(shí)調(diào)用回調(diào)函數(shù)的呢?答案是事件循環(huán)。
基于事件循環(huán)的異步編程模型
既然使用回調(diào)函數(shù)來(lái)進(jìn)行對(duì) I/O 數(shù)據(jù)的處理,就必然涉及到何時(shí)、如何調(diào)用回調(diào)函數(shù)的問(wèn)題。在實(shí)際開(kāi)發(fā)中,往往會(huì)涉及到多個(gè)、多類(lèi)異步 I/O 調(diào)用的場(chǎng)景,如何合理安排這些異步 I/O 回調(diào)的調(diào)用,確保異步回調(diào)的有序進(jìn)行是一個(gè)難題,而且,除了異步 I/O 之外,還存在定時(shí)器這類(lèi)非 I/O 的異步調(diào)用,這類(lèi) API 實(shí)時(shí)性強(qiáng),優(yōu)先級(jí)相應(yīng)地更高,如何實(shí)現(xiàn)不同優(yōu)先級(jí)回調(diào)地調(diào)度呢?
因此,必須存在一個(gè)調(diào)度機(jī)制,對(duì)不同優(yōu)先級(jí)、不同類(lèi)型的異步任務(wù)進(jìn)行協(xié)調(diào),確保這些任務(wù)在主線程上有條不紊地運(yùn)行。與瀏覽器一樣,Node 選擇了事件循環(huán)來(lái)承擔(dān)這項(xiàng)重任。
Node 根據(jù)任務(wù)的種類(lèi)和優(yōu)先級(jí)將它們分為七類(lèi):Timers、Pending、Idle、Prepare、Poll、Check、Close。對(duì)于每類(lèi)任務(wù),都存在一個(gè)先進(jìn)先出的任務(wù)隊(duì)列來(lái)存放任務(wù)及其回調(diào)(Timers 是用小頂堆存放)。基于這七個(gè)類(lèi)型,Node 將事件循環(huán)的執(zhí)行分為如下七個(gè)階段:
timers
這個(gè)階段的執(zhí)行優(yōu)先級(jí)是最高的。
事件循環(huán)在這個(gè)階段會(huì)檢查存放定時(shí)器的數(shù)據(jù)結(jié)構(gòu)(最小堆),對(duì)其中的定時(shí)器進(jìn)行遍歷,逐個(gè)比較當(dāng)前時(shí)間和過(guò)期時(shí)間,判斷該定時(shí)器是否過(guò)期,如果過(guò)期的話,就將該定時(shí)器的回調(diào)函數(shù)取出并執(zhí)行。
pending
該階段會(huì)執(zhí)行網(wǎng)絡(luò)、IO 等異常時(shí)的回調(diào)。一些 *nix
上報(bào)的錯(cuò)誤,在這個(gè)階段會(huì)得到處理。另外,一些應(yīng)該在上輪循環(huán)的 poll 階段執(zhí)行的 I/O 回調(diào)會(huì)被推遲到這個(gè)階段執(zhí)行。
idle、prepare
這兩個(gè)階段僅在事件循環(huán)內(nèi)部使用。
poll
檢索新的 I/O 事件;執(zhí)行與 I/O 相關(guān)的回調(diào)(除了關(guān)閉回調(diào)、定時(shí)器調(diào)度的回調(diào)和 之外幾乎所有回調(diào)setImmediate()
);節(jié)點(diǎn)會(huì)在適當(dāng)?shù)臅r(shí)候阻塞在這里。
poll,即輪詢(xún)階段是事件循環(huán)最重要的階段,網(wǎng)絡(luò) I/O、文件 I/O 的回調(diào)都主要在這個(gè)階段被處理。該階段有兩個(gè)主要功能:
計(jì)算該階段應(yīng)該阻塞和輪詢(xún) I/O 的時(shí)間。
處理 I/O 隊(duì)列中的回調(diào)。
當(dāng)事件循環(huán)進(jìn)入 poll 階段并且沒(méi)有設(shè)置定時(shí)器時(shí):
如果輪詢(xún)隊(duì)列不為空,則事件循環(huán)將遍歷該隊(duì)列,同步地執(zhí)行它們,直到隊(duì)列為空或達(dá)到可執(zhí)行的最大數(shù)量。
如果輪詢(xún)隊(duì)列為空,則會(huì)發(fā)生另外兩種情況之一:
如果有
setImmediate()
回調(diào)需要執(zhí)行,則立即結(jié)束 poll 階段,并進(jìn)入 check 階段以執(zhí)行回調(diào)。如果沒(méi)有
setImmediate()
回調(diào)需要執(zhí)行,事件循環(huán)將停留在該階段以等待回調(diào)被添加到隊(duì)列中,然后立即執(zhí)行它們。在超時(shí)時(shí)間到達(dá)前,事件循環(huán)會(huì)一直停留等待。之所以選擇停留在這里是因?yàn)?Node 主要是處理 IO 的,這樣可以更及時(shí)地響應(yīng) IO。
一旦輪詢(xún)隊(duì)列為空,事件循環(huán)將檢查已達(dá)到時(shí)間閾值的定時(shí)器。如果有一個(gè)或多個(gè)定時(shí)器達(dá)到時(shí)間閾值,事件循環(huán)將回到 timers 階段以執(zhí)行這些定時(shí)器的回調(diào)。
check
該階段會(huì)依次執(zhí)行 setImmediate()
的回調(diào)。
close
該階段會(huì)執(zhí)行一些關(guān)閉資源的回調(diào),如 socket.on('close', ...)
。該階段晚點(diǎn)執(zhí)行也影響不大,優(yōu)先級(jí)最低。
當(dāng) Node 進(jìn)程啟動(dòng)時(shí),它會(huì)初始化事件循環(huán),執(zhí)行用戶(hù)的輸入代碼,進(jìn)行相應(yīng)異步 API 的調(diào)用、計(jì)時(shí)器的調(diào)度等等,然后開(kāi)始進(jìn)入事件循環(huán):
┌───────────────────────────┐
┌─>│ timers │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ pending callbacks │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
│ │ idle, prepare │
│ └─────────────┬─────────────┘ ┌───────────────┐
│ ┌─────────────┴─────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └─────────────┬─────────────┘ │ data, etc. │
│ ┌─────────────┴─────────────┐ └───────────────┘
│ │ check │
│ └─────────────┬─────────────┘
│ ┌─────────────┴─────────────┐
└──┤ close callbacks │
└───────────────────────────┘
事件循環(huán)的每一輪循環(huán)(通常被稱(chēng)為 tick),會(huì)按照如上給定的優(yōu)先級(jí)順序進(jìn)入七個(gè)階段的執(zhí)行,每個(gè)階段會(huì)執(zhí)行一定數(shù)量的隊(duì)列中的回調(diào),之所以只執(zhí)行一定數(shù)量而不全部執(zhí)行完,是為了防止當(dāng)前階段執(zhí)行時(shí)間過(guò)長(zhǎng),避免下一個(gè)階段得不到執(zhí)行。
OK,以上就是事件循環(huán)的基本執(zhí)行流程?,F(xiàn)在讓我們來(lái)看另外一個(gè)問(wèn)題。
對(duì)于以下這個(gè)場(chǎng)景:
const server = net.createServer(() => {}).listen(8080); server.on('listening', () => {});
當(dāng)服務(wù)成功綁定到 8000 端口,即 listen()
成功調(diào)用時(shí),此時(shí) listening
事件的回調(diào)還沒(méi)有綁定,因此端口成功綁定后,我們所傳入的 listening
事件的回調(diào)并不會(huì)執(zhí)行。
再思考另外一個(gè)問(wèn)題,我們?cè)陂_(kāi)發(fā)中可能會(huì)有一些需求,如處理錯(cuò)誤、清理不需要的資源等等優(yōu)先級(jí)不是那么高的任務(wù),如果以同步的方式執(zhí)行這些邏輯,就會(huì)影響當(dāng)前任務(wù)的執(zhí)行效率;如果以異步的方式,比如以回調(diào)的形式傳入 setImmediate()
又無(wú)法保證它們的執(zhí)行時(shí)機(jī),實(shí)時(shí)性不高。那么要如何處理這些邏輯呢?
基于這幾個(gè)問(wèn)題,Node 參考了瀏覽器,也實(shí)現(xiàn)了一套微任務(wù)的機(jī)制。在 Node 中,除了調(diào)用 new Promise().then()
所傳入的回調(diào)函數(shù)會(huì)被封裝成微任務(wù)外,process.nextTick()
的回調(diào)也會(huì)被封裝成微任務(wù),并且后者的執(zhí)行優(yōu)先級(jí)比前者高。
有了微任務(wù)后,事件循環(huán)的執(zhí)行流程又是怎么樣的呢?換句話說(shuō),微任務(wù)的執(zhí)行時(shí)機(jī)在什么時(shí)候?
在 node 11 及 11 之后的版本,一旦執(zhí)行完一個(gè)階段里的一個(gè)任務(wù)就立刻執(zhí)行微任務(wù)隊(duì)列,清空該隊(duì)列。
在 node11 之前執(zhí)行完一個(gè)階段后才開(kāi)始執(zhí)行微任務(wù)。
因此,有了微任務(wù)后,事件循環(huán)的每一輪循環(huán),會(huì)先執(zhí)行 timers 階段的一個(gè)任務(wù),然后按照先后順序清空 process.nextTick()
和 new Promise().then()
的微任務(wù)隊(duì)列,接著繼續(xù)執(zhí)行 timers 階段的下一個(gè)任務(wù)或者下一個(gè)階段,即 pending 階段的一個(gè)任務(wù),按照這樣的順序以此類(lèi)推。
利用 process.nextTick()
,Node 就可以解決上面的端口綁定問(wèn)題:在 listen()
方法內(nèi)部,listening
事件的發(fā)出會(huì)被封裝成回調(diào)傳入 process.nextTick()
中,如下偽代碼所示:
function listen() { // 進(jìn)行監(jiān)聽(tīng)端口的操作 ... // 將 `listening` 事件的發(fā)出封裝成回調(diào)傳入 `process.nextTick()` 中 process.nextTick(() => { emit('listening'); }); };
在當(dāng)前代碼執(zhí)行完畢后便會(huì)開(kāi)始執(zhí)行微任務(wù),從而發(fā)出 listening
事件,觸發(fā)該事件回調(diào)的調(diào)用。
一些注意事項(xiàng)
由于異步本身的不可預(yù)知性和復(fù)雜性,在使用 Node 提供的異步 API 的過(guò)程中,盡管我們已經(jīng)掌握了事件循環(huán)的執(zhí)行原理,但是仍可能會(huì)有一些不符合直覺(jué)或預(yù)期的現(xiàn)象產(chǎn)生。
比如定時(shí)器(setTimeout
、setImmediate
)的執(zhí)行順序會(huì)因?yàn)檎{(diào)用它們的上下文而有所不同。如果兩者都是從頂層上下文中調(diào)用的,那么它們的執(zhí)行時(shí)間取決于進(jìn)程或機(jī)器的性能。
我們來(lái)看以下這個(gè)例子:
setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); });
以上代碼的執(zhí)行結(jié)果是什么呢?按照我們剛才對(duì)事件循環(huán)的描述,你可能會(huì)有這樣的答案:由于 timers 階段會(huì)比 check 階段先執(zhí)行,因此 setTimeout()
的回調(diào)會(huì)先執(zhí)行,然后再執(zhí)行 setImmediate()
的回調(diào)。
實(shí)際上,這段代碼的輸出結(jié)果是不確定的,可能先輸出 timeout,也可能先輸出 immediate。這是因?yàn)檫@兩個(gè)定時(shí)器都是在全局上下文中調(diào)用的,當(dāng)事件循環(huán)開(kāi)始運(yùn)行并執(zhí)行到 timers 階段時(shí),當(dāng)前時(shí)間可能大于 1 ms,也可能不足 1 ms,具體取決于機(jī)器的執(zhí)行性能,因此 setTimeout()
在第一個(gè) timers 階段是否會(huì)被執(zhí)行實(shí)際上是不確定的,因此才會(huì)出現(xiàn)不同的輸出結(jié)果。
(當(dāng) delay
(setTimeout
的第二個(gè)參數(shù))的值大于 2147483647
或小于 1
時(shí), delay
會(huì)被設(shè)置為 1
。)
我們接著看下面這段代碼:
const fs = require('fs'); fs.readFile(__filename, () => { setTimeout(() => { console.log('timeout'); }, 0); setImmediate(() => { console.log('immediate'); }); });
可以看到,在這段代碼中兩個(gè)定時(shí)器都被封裝成回調(diào)函數(shù)傳入 readFile
中,很明顯當(dāng)該回調(diào)被調(diào)用時(shí)當(dāng)前時(shí)間肯定大于 1 ms 了,所以 setTimeout
的回調(diào)會(huì)比 setImmediate
的回調(diào)先得到調(diào)用,因此打印結(jié)果為:timeout immediate
。
以上是在使用 Node 時(shí)需要注意的與定時(shí)器相關(guān)的事項(xiàng)。除此之外,還需注意 process.nextTick()
與 new Promise().then()
還有 setImmediate()
的執(zhí)行順序,由于這部分比較簡(jiǎn)單,前面已經(jīng)提到過(guò),就不再贅述了。
總結(jié)
文章開(kāi)篇從為什么要異步、如何實(shí)現(xiàn)異步兩個(gè)角度出發(fā),較詳細(xì)地闡述了 Node 事件循環(huán)的實(shí)現(xiàn)原理,并提到一些需要注意的相關(guān)事項(xiàng),希望對(duì)你有所幫助。
如果覺(jué)得這篇文章寫(xiě)的不錯(cuò)的話,就請(qǐng)給我點(diǎn)個(gè)贊吧!
參考資料
《深入淺出 Node.js》第 3 章。
到此這篇關(guān)于Node異步和事件循環(huán)的文章就介紹到這了,更多相關(guān)Node異步和事件循環(huán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Express + Node.js實(shí)現(xiàn)登錄攔截器的實(shí)例代碼
本篇文章主要介紹了Express + Node.js實(shí)現(xiàn)攔截器的實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-07-07WebSocket實(shí)現(xiàn)簡(jiǎn)單客服聊天系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了WebSocket實(shí)現(xiàn)簡(jiǎn)單客服聊天系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2017-05-05module.exports和exports使用誤區(qū)案例分析
module.exports和exports使用誤區(qū),使用require()模塊時(shí),得到的永遠(yuǎn)都是module.exports指向的對(duì)象2023-04-04window系統(tǒng) nodejs安裝opencv環(huán)境配置圖文詳解
這篇文章主要介紹了window系統(tǒng) nodejs安裝opencv環(huán)境配置,結(jié)合圖文形式詳細(xì)分析了window環(huán)境下 nodejs安裝opencv的具體步驟、注意事項(xiàng)2023-04-04使用Node.js實(shí)現(xiàn)一個(gè)多人游戲服務(wù)器引擎
這篇文章主要給大家介紹了關(guān)于如何使用Node.js實(shí)現(xiàn)一個(gè)多人游戲服務(wù)器引擎的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用Node.js具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-03-03NodeJs操作MongoDB教程之分頁(yè)功能以及常見(jiàn)問(wèn)題
這篇文章主要給大家介紹了關(guān)于NodeJs操作MongoDB教程之分頁(yè)功能以及常見(jiàn)問(wèn)題的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用NodeJs具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-04-04使用node.js 制作網(wǎng)站前臺(tái)后臺(tái)
本文給大家介紹實(shí)用node.js 制作網(wǎng)站前臺(tái)和后臺(tái),非常的詳盡,有需要的朋友可以參考下2014-11-11