函數(shù)式編程
在JavaScript中,函數(shù)作為一等公民,使用上十分自由,無論調(diào)用它,或者作為參數(shù),或者作為返回值均可。
高階函數(shù)
高階函數(shù)是可以把函數(shù)作為參數(shù),或是將函數(shù)作為返回值的函數(shù)。函數(shù)式編程就是指這種高度抽象的編程范式,是JavaScript異步編程的基礎(chǔ)。
變量指向函數(shù),函數(shù)名也是變量。
function foo(x, bar) {
return bar(x);
}
對于相同的foo()函數(shù),傳入的bar參數(shù)不同,則可以得到不同的結(jié)果。這這里可以看出高階函數(shù)的靈活性,結(jié)合Node提供的最基本的事件模塊可以看到,事件的處理方式正是基于高階函數(shù)的特性來完成的,在自定義事件實(shí)例中,通過為相同事件注冊不同的回調(diào)函數(shù),可以很靈活地處理業(yè)務(wù)邏輯。
var emitter = new events.EventEmitter();
emitter.on('event_foo', function(){
// todo something
})
偏函數(shù)用法
var toString = Object.prototype.toString;
var isString = function (obj) {
return toString.call(obj) == '[object String]';
}
var isFunction = function (obj) {
return toString.call(obj) == '[object Function]';
}
// 改寫為
var isType = function(type) {
return function(obj) {
return toString.call(obj) == `[object ${type}]`
}
}
var isString = isType('String')
var isFunction = isType('Function')
這種通過指定部分參數(shù)來產(chǎn)生一個新的定制函數(shù)的形式就是偏函數(shù)應(yīng)用。
固定一個函數(shù)的一些參數(shù),然后產(chǎn)生另一個更小元的函數(shù)。
什么是元?元是指函數(shù)參數(shù)的個數(shù),比如一個帶有兩個參數(shù)的函數(shù)被稱為二元函數(shù)。
函數(shù)柯里化
柯里化是將一個多參數(shù)函數(shù)轉(zhuǎn)換成多個單參數(shù)函數(shù),也就是將一個 n 元函數(shù)轉(zhuǎn)換成 n 個一元函數(shù)。
接受多個參數(shù)的函數(shù)變換成接受一個單一參數(shù)的函數(shù),返回接受處理余下的參數(shù)且返回結(jié)果的新函數(shù)。
// 示意而已
function ajax(type, url, data) {
var xhr = new XMLHttpRequest();
xhr.open(type, url, true);
xhr.send(data);
}
// 雖然 ajax 這個函數(shù)非常通用,但在重復(fù)調(diào)用的時候參數(shù)冗余
ajax('POST', 'www.test.com', "name=kevin")
ajax('POST', 'www.test2.com', "name=kevin")
ajax('POST', 'www.test3.com', "name=kevin")
// 利用 curry
var ajaxCurry = curry(ajax);
// 以 POST 類型請求數(shù)據(jù)
var post = ajaxCurry('POST');
post('www.test.com', "name=kevin");
// 以 POST 類型請求來自于 www.test.com 的數(shù)據(jù)
var postFromTest = post('www.test.com');
postFromTest("name=kevin");
可以理解為:參數(shù)復(fù)用。本質(zhì)上是降低通用性,提高適用性。
var person = [{name: 'kevin'}, {name: 'daisy'}]
var name = person.map(function (item) {
return item.name;
})
// 使用curry
var prop = curry(function (key, item) {
return item[key]
});
var name = person.map(prop('name'))
// prop('name') 此時就是
function (item) {
return item['name'];
}
function curry(fn, args) {
var length = fn.length;
args = args || [];
return function(...innerArgs) {
var _args = [...args, ...innerArgs];
if (_args.length < length) {
return curry.call(this, fn, _args);
}
return fn.apply(this, _args);
}
}
var curry = fn => judge = (...args) => args.length === fn.length ? fn(...args) : (...remainingArg) => judge(...args, ...remainingArg);
異步編程的優(yōu)勢和難點(diǎn)
優(yōu)勢
Node帶來的最大特性莫過于基于事件驅(qū)動的非阻塞I/O模型,非阻塞I/O可以使CPU和I/O并不相互依賴等待,讓資源得到更好的應(yīng)用。
Node實(shí)現(xiàn)異步I/O的原理:利用時間循環(huán)的方式,JavaScript線程像一個分配任務(wù)和處理結(jié)果的大管家,I/O線程池里的各個I/O線程都是小二,負(fù)責(zé)完成分配來的任務(wù),小二和管家之間互不依賴,所以可以保持整體的高效率。
這個模型的缺點(diǎn)則在于管家無法承擔(dān)過多的細(xì)節(jié)性任務(wù),如果承擔(dān)太多,則會影響任務(wù)的調(diào)度。由于事件循環(huán)模型需要應(yīng)對海量請求,海量請求同時作用于在單線程上,就需要放置任何一個計算耗費(fèi)過多的CPU時間片,至于是計算密集型還是I/O密集型,只要不影響異步I/O的調(diào)度,那就不構(gòu)成問題。
難點(diǎn)1:異常處理
異步I/O的實(shí)現(xiàn)主要包含兩個階段,提交請求和處理結(jié)果,這兩個階段中間有事件循環(huán),兩者彼此不關(guān)聯(lián),異步方法則通常在第一個階段提交請求后立即返回,因此異常并不一定發(fā)生在這個階段,所以如果使用try/catch包裹提交請求這一階段,那么對處理結(jié)果的callback執(zhí)行時拋出的異常將無能為力。
Node在處理異常上形成了一種約定,將異常作為回調(diào)函數(shù)的第一個實(shí)參傳回,如果為空值,則表明異步調(diào)用沒有異常拋出。
在我們執(zhí)行編寫的異步方法上,也需要去遵循這樣一些原則:
原則一:必須執(zhí)行調(diào)用者傳入的回調(diào)函數(shù)
原則二:正確傳遞回異常功調(diào)用者判斷
var asynsFn = function(callback) {
try {
// do something
var result = xxx;
} catch(err) {
err.status = 400;
return callback(err);
}
callback(null, result)
}
難點(diǎn)2:嵌套過深
對于Node而言,事務(wù)中存在多個異步調(diào)用的場景比比皆是,如果操作存在依賴關(guān)系,那么函數(shù)嵌套行為情有可原,如果操作不存在依賴關(guān)系,那么在一個異步調(diào)用的回調(diào)中調(diào)用另外一個異步的操作也應(yīng)該盡量避免(如下例),因?yàn)檫@沒有利用好異步I/O帶來的并行優(yōu)勢。
fs.readFile(template_path, 'utf8', function (err, template) {
db.query(sql, function (err, data) {
l10n.get(function (err, resources) {
// TODO
});
});
});
Node提供了絕大部分的異步API和少量的同步API,那么回調(diào)也必不可少,編寫代碼時不注意就很可能會造成過深的嵌套。
難點(diǎn)3:阻塞代碼
在JavaScript中沒有sleep()這樣的線程沉睡功能,所以多半開發(fā)者會寫出下述這樣的代碼來實(shí)現(xiàn)sleep(1000)的效果
var start = new Date();
while (new Date() - start < 1000) {
}
// TODO 需要阻塞的代碼
但是事實(shí)是糟糕的,這段代碼會持續(xù)占用CPU進(jìn)行判斷,與真正的線程沉睡相去甚遠(yuǎn),完全破壞了事件循環(huán)的調(diào)度,由于Node單線程的原因,CPU資源全都會用于為這段代碼服務(wù),導(dǎo)致其余任何請求都會得不到響應(yīng),所以在這種情況下,使用setTimeout()的效果會更好。
難點(diǎn)4:多線程編程
在瀏覽器中的單線程指的是JavaScript執(zhí)行線程與UI渲染共用的一個線程,在Node中,只是沒有UI渲染部分,模型基本相同。
對于服務(wù)器端而言,如果服務(wù)器是多核CPU,單個Node進(jìn)程實(shí)質(zhì)上是沒有充分利用多核CPU的,隨著業(yè)務(wù)的復(fù)雜化,瀏覽器提出了Web Workers,它將通過將JavaScript執(zhí)行與UI渲染分離,可以很好的利用多核CPU為大量計算服務(wù)。同時前端Web Workers也是一個利用消息機(jī)制合理使用多核CPU的理想模型。
Node借鑒了這個模式,child_process是其基礎(chǔ)API,cluster模塊是更深層次的應(yīng)用,因此,如果使用這種模型,開發(fā)人員需要更多地去面臨跨線程的編程,這對于以往的JavaScript編程經(jīng)驗(yàn)是較少考慮的。
難點(diǎn)5:異步轉(zhuǎn)同步
Node的異步API占了絕大部分,偶爾出現(xiàn)的同步需求將會因?yàn)闆]有同步API讓開發(fā)者無所適從,目前,Node中試圖同步式編程,但并不能得到原生支持,需要借助庫或者編譯手段來實(shí)現(xiàn),但對于異步調(diào)用,通過良好的流程控制,還是能夠?qū)⑦壿嬍崂沓身樞蚴降男问健?/p>