周安輝
(內(nèi)江職業(yè)技術(shù)學(xué)院,四川 內(nèi)江 641100)
Node.js是一個編寫網(wǎng)絡(luò)服務(wù)和網(wǎng)頁應(yīng)用的平臺,采用C++語言編寫,優(yōu)化了Google V8引擎,能夠高效地運(yùn)行JavaScript代碼,同時提供了文件、網(wǎng)絡(luò)等眾多系統(tǒng)級的API,有助于開發(fā)人員快速地構(gòu)建高性能的網(wǎng)絡(luò)服務(wù)及其應(yīng)用。
Node.js圍繞一個事件驅(qū)動的無阻塞I/O的異步編程模式而構(gòu)建,代碼執(zhí)行無須阻塞等待某種低速的I/O操作完成而繼續(xù),充分利用了有限的資源,非常適合編寫處理大量并發(fā)請求的后臺網(wǎng)絡(luò)服務(wù)。此外,服務(wù)器端與客戶端的編寫,統(tǒng)一使用JavaScript語言,受到開發(fā)人員的極大歡迎。
基于函數(shù)的傳統(tǒng)編程,開發(fā)人員是相當(dāng)熟悉的,大部分編程語言都使用,Node.js也不例外,但是要注意一些概念上的區(qū)別。在Node.js的編程中,要正確理解以下幾個基本的函數(shù)概念,可以幫助我們掌握Node.js的編程模式。
在Node.js中可以在定義一個函數(shù)后立即執(zhí)行它。只需要簡單地用()括號包裹函數(shù),并調(diào)用它,如下所示:.
(function myData(){
console.log('myData was executed!');
})();
在 JavaScript中,if、else或 while語句體并不會創(chuàng)建一個新的變量作用域。如下所示:
var myData=123;
if(true){
var myData=456;
}
console.log(myData);//456;
在JavaScript中,只有使用一個立即執(zhí)行函數(shù)會創(chuàng)建一個新的變量作用域。如下所示:
var myData=123;
if(true){
(function(){//create a new scope
var myData=456;
console.log(myData);//456;
})();
}
console.log(myData);//123;
一個沒有名字的函數(shù)被稱為匿名函數(shù)。在JavaScript中,你可以指派一個函數(shù)給一個變量。如果你打算把一個函數(shù)賦值給一個變量,你不需要使用命名函數(shù)。
以下兩種方式定義一個內(nèi)聯(lián)函數(shù),兩者是等價的:
var foo1=function namedFunction(){
console.log('foo1');
}/*www.java2s.com*/
foo1();//foo1
var foo2=function(){//no function name i.e.anonymous function
console.log('foo2');
}
foo2();//foo2
JavaScript語言擁有首類函數(shù)。首類函數(shù)意味著函數(shù)被當(dāng)作對象一樣的東西來看待,可以把它指派給一個變量。
高階函數(shù)
因?yàn)镴avaScript語言允許指派函給變量,所以能夠傳送函數(shù)給其他函數(shù)。高階函數(shù)意味著使用其他函數(shù)作參數(shù)或者返回一個函數(shù)作結(jié)果。setTimeout是一個很常見的高階函數(shù)例子,用法如下:.
setTimeout(function(){
console.log('2000 milliseconds have passed since this demo started');
},2000);
在Node.js運(yùn)行這個代碼,你會在兩秒后才看見控制臺日志消息。
在setTimeout中使用了一個匿名函數(shù)作為第一個參數(shù),這讓setTimeout成為一個高階函數(shù)。
也可定義一個函數(shù),顯式傳遞給setTimeout,如下所示:
function foo(){
console.log('2000 milliseconds have passed since this demo started');
}
setTimeout(foo,2000);
這個概念是非常直觀和簡單。如果一個函數(shù)定義在另外一個函數(shù)的內(nèi)部,內(nèi)部函數(shù)要訪問外部函數(shù)聲明的變量。如下所示:
function outerFunction(arg){
var variableInOuterFunction=arg;
function myValue(){
console.log(variableInOuterFunction);
}
myValue();
}
outerFunction ('hello closure!');//logs hello closure!
即使外部函數(shù)已經(jīng)返回,內(nèi)部函數(shù)還是能夠訪問外部作用域的變量。因?yàn)樵撟兞咳匀槐粌?nèi)部函數(shù)綁定,并不依賴于外部函數(shù)。如下所示:
function outerFunction(arg){
var variableInOuterFunction=arg;return function(){
console.lo(variableInOuterFunction);
}
}
var innerFunction = outerFunction('hello closure!');
innerFunction();
Node.js異步編程采用后續(xù)傳遞風(fēng)格(continuation-passing style,CPS),編寫的CPS函數(shù)有一個顯式的“后續(xù)”函數(shù)作為額外參數(shù),在調(diào)用CPS函數(shù)計算出返回值時,并不表示函數(shù)結(jié)束,而將CPS函數(shù)的返回值作為“后續(xù)”函數(shù)的參數(shù),繼續(xù)調(diào)用“后續(xù)”函數(shù),顯示地將流程控制權(quán)傳遞給“后續(xù)”函數(shù)。
在后續(xù)傳遞風(fēng)格的編程中,每個函數(shù)在執(zhí)行完畢后都會調(diào)用一個回調(diào)函數(shù),將程序繼續(xù)進(jìn)行下去。如你所見,在JavaScript就是采用這種方式編程,例如Node.js中,將input.txt文件加載到內(nèi)存并顯示出的例子:
var fs=require('fs');
fs.readFile ('./input.txt',function(err,data){
if(err){
console.log(err.stack);
return; }
console.log(' 文件內(nèi)容: ',data.toString());
});
console.log('Reading file...');
執(zhí)行這段代碼,首先會顯示'Reading file...'字符串,然后等待回調(diào)函數(shù)的結(jié)果返回后,才會顯示文件內(nèi)容,這是一種典型的異步執(zhí)行模式。
注意:內(nèi)聯(lián)匿名回調(diào)函數(shù)的第一個參數(shù)是一個錯誤對象,如果有錯誤發(fā)生,其為Error類的一個實(shí)例,這是Node.js中應(yīng)用CPS編程的一個通用模式。
使用異步方法并不能保證執(zhí)行次序,下面的例子是我們經(jīng)常犯的錯誤:
var fs=require('fs');
fs.rename('/tmp/hello','/tmp/world',(err)=>{
if(err)throw err;
console.log('renamed complete');
});
fs.stat('/tmp/world',(err,stats)=>{
if(err)throw err;
console.log(`stats:${JSON.stringify(stats)}`);
});
fs.stat?可能在fs.rename之前被執(zhí)行。要保證流程控制權(quán)的正確執(zhí)行次序,正確的做法是采用鏈?zhǔn)交卣{(diào)函數(shù),如下所示:
var fs=require('fs');
fs.rename('/tmp/hello','/tmp/world',(err)=>{
if(err)throw err;
fs.stat('/tmp/world',(err,stats)=>{
if(err)throw err;
console.log(`stats:${JSON.stringify(stats)}`);
});
});
Node.js大量使用事件來決定程序的流程控制權(quán),使它與其他采用“事件驅(qū)動編程”相似技術(shù)相比較,Node.js就顯得更快更高效。Node.js一旦啟動了它的服務(wù)器,它僅是簡單地初始變量,聲明函數(shù),然后就只需等待事件發(fā)生。
標(biāo)準(zhǔn)回調(diào)模式是單事件工作模式,在異步函數(shù)返回其結(jié)果時觸發(fā)調(diào)用回調(diào)函數(shù)。如果是在函數(shù)的執(zhí)行中發(fā)生了多個事件或事件重復(fù)發(fā)生,這種模式就不是很理想了,而事件驅(qū)動模式則在這種情形下很好工作。一般而言,在需要請求的操作完成后要重獲流程控制權(quán),采用標(biāo)準(zhǔn)回調(diào)模式,而當(dāng)多個事件發(fā)生或事件重復(fù)發(fā)生時,要決定流程控制權(quán),采用事件驅(qū)動模式。本質(zhì)上,可以把Node.js標(biāo)準(zhǔn)回調(diào)模式視為特定的單事件驅(qū)動編程模式。
在事件驅(qū)動模式編程中,偵聽事件的函數(shù)充當(dāng)觀察器,只有事件發(fā)生器發(fā)射一個事件被觀察到時,它的偵聽器的回調(diào)函數(shù)才開始執(zhí)行。
下面的代碼,create_websever.js用于創(chuàng)建一個web服務(wù)器,ex2_event.js演示請求web頁面時,并對response發(fā)射的data與end內(nèi)置事件類型進(jìn)行響應(yīng):
create_websever.js文件如下所示:
const http=require('http');
const hostname='127.0.0.1';
const port=3000;
const server=http.createServer((req,res)=>{res.statusCode=200;
res.setHeader('Content-Type','text/plain');
res.end('Hello World ');
});
server.listen(port,hostname,()=>{
console.log(`Server running at http://${hostname}:${port}/`);
});
ex2_event.js文件:
var http=require('http');
var options={
host:'127.0.0.1',
port:3000,
path:'/'
};
var req=http.request(options,function(res){res.setEncoding('utf8');
res.on('data',function(data){console.log('some data from the response',data);
});
res.on('end',function(){console.log('response ended');
});
})
req.end();
Node.js?使用events模塊和?EventEmitter?類實(shí)現(xiàn)自定義事件類型編程。通過?EventEmitter?類來綁定事件與事件偵聽器,可以實(shí)現(xiàn)多個自定義事件類型的發(fā)射和偵聽。如下代碼所示:
//Import events module
var events=require('events');
//Create an eventEmitter object
var eventEmitter=newevents.EventEmitter();
//Create an event handler as follows
var connectHandler=function connected(){console.log('connection succesful.');
//Fire the data_received event
eventEmitter.emit('data_received');}
//Bind the connection event with the handler
eventEmitter.on('connection',connectHandler);
//Bind the data_received event with the anonymous function
eventEmitter.on ('data_received',function(){
console.log('data received succesfully.');});
//Fire the connection event
eventEmitter.emit('connection');
console.log(“Program Ended.”);
在Node中,事件發(fā)生器采用通用接口服務(wù)各種類型的事件,但是“error”事件除外,大部分Node事件發(fā)生器在程序產(chǎn)生錯誤時都要產(chǎn)生“error”事件。如果不監(jiān)聽該事件,“error”事件發(fā)生時,Node事件發(fā)生器會拋出一個未捕獲的異常,顯示一個堆棧追蹤,而且Node進(jìn)程會退出。
最佳實(shí)踐是始終偵聽“error”事件,如下所示:
var myEmitter=new(require('events').EventEmitter)();
myEmitter.on('error',(err)=>{
console.log('whoops!there was an error');
});
myEmitter.emit ('error', new Error('whoops!'));
Node.js的event loop,后臺采用Libuv[4]高性能的事件輪詢模型,負(fù)責(zé)調(diào)度回調(diào)函數(shù)隊列的執(zhí)行,是實(shí)現(xiàn)非阻塞I/O異步編程的關(guān)鍵機(jī)制。當(dāng)Node.js啟動時,將初始化event loop,處理那些可能做異步API調(diào)用、定制計時器或調(diào)用process.nextTick()的輸入腳本,然后開始處理event loop。
event loop包括六個循環(huán)階段,如下圖1所示:
圖1 六個循環(huán)階
每個階段都有一個可執(zhí)行的回調(diào)函數(shù)的FIFO隊列。盡管每個階段具有自己特殊方式,通常,當(dāng)事件循環(huán)進(jìn)入一個給定的階段,它將執(zhí)行這個階段的任何特定操作,然后執(zhí)行在這個階段的隊列中的回調(diào)函數(shù),直到隊列為空或者回調(diào)函數(shù)數(shù)量達(dá)到上限,event loop會進(jìn)入到下一下階段,等等,細(xì)節(jié)可參考官方文檔[3]。
Node.js后續(xù)傳遞風(fēng)格的編程,看上去很丑陋,并且與傳統(tǒng)的編程思維模式相違背,讓人入手時難以適應(yīng),只有當(dāng)你深入理解事件輪詢event loop的基本原理后,對于Node.js后續(xù)傳遞風(fēng)格的異步編程會有較大幫助,并且會逐步喜歡上它的簡明與高效。