函數參數只接受基本數據類型或者對象引用,返回值也是基本數據類型和對象引用。javascript
//常規參數傳遞和返回
function foo(x) {
return x;
}
複製代碼
高階函數則是能夠把函數做爲參數和返回值的函數。前端
function foo(x) {
return function() {
return x
}
}
複製代碼
function foo(x, bar) {
return bar(x);
}
複製代碼
上面這個函數相同的foo函數可是傳入的bar參數不一樣則能夠返回不一樣的結果,列如數組的sort()方法,它是一個高階函數,接受一個參數方法做爲排序運算。java
var arr = [40, 80, 60, 5, 30, 77];
arr.sort(function(a, b) {
return a - b;
});
//運行結果[5, 30, 40, 60, 77, 80];
複製代碼
經過修改sort方法的參數能夠決定不一樣的排序方法,這就是高階函數的靈活性。
結合Node的基本事件模塊能夠看到,事件處理方式正是基於高階函數的特性來完成的。在自定義事件中,經過爲相同的事件註冊不一樣的回調函數,能夠很靈活的處理業務邏輯。jquery
var emit = new events.EventEmitter();
emit.on('event', function() {
//do something
});
複製代碼
偏函數指的是建立一個調用另一部分(參數或者變量已經預置的函數)的函數的用法例如:sql
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]';
}
複製代碼
這段代碼用於判斷類型,一般會進行上述定義,代碼存在類似性若是要判斷更多會定義更多的isXXX()方法,這樣就會出現冗餘代碼。爲了解決重複問題,引入一個新函數用於批量建立這樣的相似函數。數據庫
var isType = function (type) {
return function(obj) {
return toString.call(obj) == '[object '+ type +']';
}
}
var isString = isType('String');
var isFunction = isType('Function');
複製代碼
這種經過指定部分參數來產生一個新的定製函數的形式就是偏函數。編程
Node的最大特性是基於事件驅動的非阻塞I/O模型,這使得CPU和I/O不互相依賴,讓資源更好的利用,對於網絡應用而言使得各個單點之間能夠更有效的組織起來,這使得Node在雲計算中廣受青睞。 因爲事件循環模型要應對海量請求,全部請求做用在單線程上須要防止任何一個計算過多的消耗CPU時間片。建議計算對CPU的耗用不超過10ms,或將大量的計算分解成小量計算,經過setImmediate()進行調度。api
一般處理異常時使用 try/catch/final語句進行異常捕獲:數組
try {
JSON.parse(str);
}catch(e) {
console.log(e)
}
複製代碼
但這對於異步編程不必定適用。I/O實現異步有兩個階段:提交請求和處理結果。這兩個階段中間有事件循環調度,彼此互不關聯,一步方法一般在第一個階段請求提交後當即返回,可是錯誤異常並不必定發生在這個階段,try/catch就不必定會發生做用了。promise
var async = function(callback) {
process.nextTick(callback);
}
try {
async(callback);
}catch(e) {
}
複製代碼
調用async方法後callback會被存起來知道下一個事件循環才被執行,try/catch操做只能捕獲當前時間循環內的異常。對callback中的異常不起做用。
Node在處理異常上造成了一個約定,將異常做爲回調的第一個參數傳回,若是是空值,代表沒有異常拋出:
async(function(err, res)) {
});
複製代碼
在自行編寫的異步方法上也須要去遵循這樣的原則: 1.必須執行調用者傳入的回調函數; 2.正確的傳回異常供調用者判斷;
var async = function(callback) {
process.nextTick(function() {
var res = 'something';
if(error) {
return callback(error);
}else {
return callback(null, res);
}
})
}
複製代碼
在異步編程中,另外一個容易犯的錯誤是對用戶傳遞的callback進行異常捕獲,
try {
req.body = JSON.parse(buf, options.reviver);
callback();
}catch(e) {
err.body = buf;
err.status = 400;
callback(e);
}
複製代碼
若是JSON.parse出現錯誤代碼將進入catch部分這樣回調函數callback將被執行兩次,正確的作法應該是
try {
req.body = JSON.parse(buf, options.reviver);
}catch(e) {
err.body = buf;
err.status = 400;
return callback(e);
}
callback();
複製代碼
Node中事物中會出現多個異步調用的場景,列如遍歷目錄:
fs.readdir('path', function(err, files) {
files.forEach(function(fileName, index) {
fs.readFile(fileName, 'utf8', function(err, file) {
//TODO
})
})
});
複製代碼
上述操做因爲兩次操做存在依賴關係,函數嵌套行爲情有可原,可是在某些場景列如渲染網頁:一般須要數據、模版、資源文件,這三個操做互不依賴可是最終結果又是三者不可缺一,若是採用默認異步調用會是這樣:
fs.readFile('path', 'utf8', function(err, templete) {
db.query(sql, function(err, data) {
l10n.get(function(err, res) {
//TODO
})
})
})
複製代碼
這樣致使了代碼嵌套過深,不易讀且很差維護。
javascript沒有sleep()這樣讓線程沉睡的功能,只有setInterval()和setTimeout()這兩個函數,可是這兩個函數不能阻塞後面的代碼執行。
說到javascript時候,一般談的是單線程上執行。隨着業務複雜,對於多核CPU的利用要求也愈來愈高。瀏覽器中提出了Web Workers。能夠更好的利用多核CPU爲大量計算服務。前端Web Workers也是利用消息機制合理的使用多核CPU的理想模型。
Node提供了絕大部分的異步API和少許的同步API,Node中試圖同步編程,但並不能獲得原生支持,須要藉助庫或者編譯手段實現。
目前,異步編程主要解決方案有三種:
Node自身提供的events模塊是發佈/訂閱的一個簡單實現,Node中部分模塊都繼承自它。它具備addListener/on()、 once()、 removeListener()、 removeAllListener()、 emit() 等基礎方法。
//訂閱
emitter.on('event', function(msg) {
console.log(msg)
});
emitter.emit('event', 'this is msg');
複製代碼
Node對事件發佈/訂閱的機制作了一些額外處理:
實現一個繼承EventEmitter的類:
var events = require('events');
function Stream() {
events.EventEmitter.call(this);
}
util.inherits(stream, events.EventEmitter);
複製代碼
Node在util模塊封裝了繼承方法。
在事件訂閱/發佈模式中,一般有一個once()方法,經過它添加的偵聽器只能執行一次,執行後將被移除。
在計算機中,緩存因爲放在內存中,訪問書的快,用於加速數據訪問,讓絕大多數請求沒必要重複去作一些低效的數據讀取。所謂的雪崩問題,就是高訪問量、大併發量的狀況下緩存失效的狀況,此時大量的請求同時涌入數據庫中,數據庫沒法承受大量查詢請求進而影響網站總體響應速度。
//查詢數據庫
var select = function(callback) {
db.select(sql, function(err, res) {
callback(res);
})
}
複製代碼
若是站點剛啓動緩存中尚未數據,若是訪問量巨大,同一句sql會被執行屢次反覆查詢數據庫,將會影響性能。改進方案:加一個狀態鎖
var status = 'ready';
var select = function(callback) {
if(status === 'ready') {
status = 'pending';
db.select(sql, function(err, res) {
status = 'ready';
callback(res);
})
}
}
複製代碼
可是在這種狀況下連續屢次調用只有第一次調用是生效的,後續調用是沒有數據服務的,這個時候能夠引入事件列隊:
var proxy = new events.EventEmitter();
var status = 'ready';
var select = function(callback) {
proxy.once('selected', callback);
if(status == 'ready') {
status = 'pending';
db.select(sql, function(err, res) {
proxy.emit('selected')
status = 'ready';
})
}
}
複製代碼
利用once()方法,將全部請求的回調壓入事件列隊,利用其執行一次就好將監視器移除的特色,保證一次回調只會被執行一次。
以上面提到的渲染網頁(模版讀取、數據讀取、本地資源讀取)爲例:
var count = 0;
var res = {};
var done = function(key, val) {
res[key] = val;
count ++;
if(count === 3) {
render(res);
}
};
fs.readFile(template_path, 'utf8', function(err, tp) {
done('tp', tp);
});
db.query(sql, (err, data) {
done('data', data);
});
l10n.get(function(err, res) {
done('res', res);
});
複製代碼
一般用於檢測次數的變量叫作'哨兵變量'。利用偏函數來處理哨兵變量和第三方函數的關係:
var after = function(times, callback) {
var count = 0,res = {};
return function(key, val) {
res[key] = val;
count ++;
if(count == times) {
callback(res);
}
}
}
var emitter = new events.EventEmitter();
var done = after(times, render);
emitter.on('done', done);
emitter.on('done', other);
fs.readFile(template_path, 'utf8', function(err, tp) {
emitter.emit('done', 'tp', tp);
});
db.query(sql, (err, data) {
emitter.emit('done', 'data', data);
});
l10n.get(function(err, res) {
emitter.emit('done', 'res', res);
});
複製代碼
撲靈寫的EventProxy模塊,是對事件訂閱/發佈模式的擴充
var proxy = new EventProxy();
proxy.all('tp', 'data', 'res', function(tp, data, res) {
//TODO
});
fs.readFile(template_path, 'utf8', function(err, tp) {
proxy.emit('tp', tp);
});
db.query(sql, (err, data) {
proxy.emit('data', data);
});
l10n.get(function(err, res) {
proxy.emit('res', res);
});
複製代碼
EventProxy提供了all()方法來訂閱多個事件,全部事件觸發後偵聽器纔會被觸發。另外一個tail()方法在知足條件時只需一次後,若是組合事件中的某個事件再次被觸發,偵聽器會用最新的數據繼續只需。
after()方法:實現事件在執行多少次後執行偵聽器的單一事件組合訂閱方式:
//執行10次data事件後觸發偵聽器
var proxy = new EventProxy();
proxy.after('data', 10, function(datas) {
//TODO
})
複製代碼
EventProxy原理
EventProxy來源自Backbone的事件模塊,它在每一個非all的事件觸發時都會觸發一次all事件
trigger: functuon(eventName) {
var list, calls, ev, callback, args;
var both = 2;
if(!(calls = this._callbacks)) return;
while (both--) {
ev = both?eventName:'all';
if(list = calls[ev]) {
for(var i = 0, l = list.length; i < 1; i ++) {
if(!(callback = list[i])) {
list.splice(i, i);
i --;
l --;
}else {
args = both? Array.prototype.slice.call(arguments, 1):argument;
callback[0].apply(callback[1] || this, args);
}
}
}
}
return this;
}
複製代碼
EventProxy則是將all當作一個事件流的攔截層,在其中注入一些業務來處理單一事件沒法解決的異步處理問題。
EventProxy提供了fail()和done()兩個實例方法來優化異常處理。
var proxy = new EventProxy();
proxy.all('tp', 'data', function(tp, data, res) {
//TODO
});
proxy.fail(function(err) {
//錯誤處理
})
fs.readFile(template_path, 'utf8', proxy.done('tp'));
db.query(sql, proxy.done('data'));
proxy.done('tp')等價於
function(err, data) {
if(err) {
return proxy.emit('error', err)
}
proxy.emit('tp', data)
}
複製代碼
使用事件的方式時,執行的流程被預先設定。Promise/Deferred模式先執行異步調用,延遲傳遞處理方式。
//普通jquery Ajax調用
$.get('api',{
success: onSuccess,
error: onError,
complete: onComplete
})
複製代碼
須要提早預設對應的回調函數
//Promise/Deferred模式 jquery Ajax調用
$.get('api')
.success(onSuccess)
.error(onError)
.complete(onComplete);
複製代碼
Promise/Deferred模式即便不傳入回調函數也能執行,傳統方法一個事件只能傳入一個回調函數,而Deferred對象能夠對事件加入任意業務處理邏輯。
$.get('api')
.success(onSuccess1)
.success(onSuccess2);
複製代碼
CommonJS抽象出了 Promises/A,Promises/B,Promises/D這樣的典型異步Promise/Deferred模型。
Promises/A提議對單個異步操做作出這樣的定義:
Promises/A API定義比較簡單。一個Promise對象只要具有then()方法便可。對應then()的要求:
then()方法定義:
then(fulfilledHandler, errorHandler, progressHandler)
複製代碼
Promises/A演示:
var Promise = function() {
EventEmitter.call(this);
};
util.inherit(Promise, EventEmitter);
Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {
if(typeof fulfilledHandler === 'function') {
this.once('success', fulfilledHandler)
}
if(typeof errorHandler === 'function') {
this.once('error', errorHandler)
}
if(typeof progressHandler === 'function') {
this.on('progress', progressHandler)
}
return this;
}
複製代碼
then()方法將回調函數存放起來,爲了完成整改流程,還須要觸發執行這些函數的地方,實現這些功能的對象被稱爲Deferred,即延遲對象:
var Deferred = function() {
this.state = 'unfulfilled';
this.promise = new Promise();
}
Deferred.prototype.resolve = function(obj) {
this.state = 'fulfilled';
this.promise.emit('success', obj);
}
Deferred.prototype.reject = function(obj) {
this.state = 'faild';
this.promise.emit('error', obj);
}
Deferred.prototype.progress = function(obj) {
this.promise.emit('progress', obj);
}
複製代碼
res.setEncoding('utf8');
res.on('data', function(chunk) {
//成功
console.log(chunk)
});
res.on('end', function() {
//失敗
})
res.on('error', function(err) {
//progress
})
//經過改造
var promisify = function(res) {
var deferred = new Deferred();
var res = '';
res.on('data', function(chunk) {
res += chunk;
deferred.progress(chunk);
})
res.on('end', function() {
promise.resovle(res);
})
res.on('error',function(err) {
promise.reject(err)
})
return deferred.promise;
}
//簡便寫法
promisify(res).then(function() {
//成功
},function(err) {
//失敗
}, function(chunk) {
//progress
})
複製代碼
從上面代碼能夠看出,Deferred主要用於內部,用於維護異步模型的狀態;Promise則做用於外部,經過then()方法暴露給外部添加自定義邏輯。
Q模塊...
簡單原型實現:
Deferred.prototype.all = function(promises) {
var count = promises.length;
var that = this;
var res = [];
promises.forEach(function(promise, i) {
promise.then(function(data) {
count --;
res[i] = data;
if(count === 0) {
that.resolve(res);
}
}, function(err) {
that.reject(err);
})
return this.promise;
})
}
複製代碼
經過all()方法抽象多個異步操做。只有全部異步操做成功,這個異步操做纔算成功,其中有一個失敗,整個異步操做就失敗。
(實際使用推薦使用when,Q模塊,是對完整的Promise提議的實現)
現有一組純異步的API,爲完成一件串聯事代碼以下:
obj.api1(function(val1) {
obj.api2(val1, function(val2) {
obj.api3(val2, function(val3) {
obj.api4(val3, function(val4) {
callback(val4);
})
})
})
})
複製代碼
使用普通函數將上面代碼展開:
var handler1 = function(val1) {
obj.api2(val1, handler2);
}
var handler2 = function(val2) {
obj.api3(val2, handler3);
}
var handler3 = function(val3) {
obj.api4(val3, handler4);
}
var handler41 = function(val4) {
callback(val4)
}
obj.api1(handler1);
複製代碼
使用事件機制
var emitter = new EventEmitter();
emitter.on('step1', function() {
obj.api1(function(val1) {
emitter.emit('step2', val1);
})
})
emitter.on('step2', function(val1) {
obj.api2(val1, function(val2) {
emitter.emit('step3', val2);
})
})
emitter.on('step3', function(val2) {
obj.api3(val2, function(val3) {
emitter.emit('step42', val3);
})
})
emitter.on('step4', function(val3) {
obj.api4(val3, function(val4) {
callback(val4);
})
})
emitter.emit('step1');
複製代碼
使用事件後代碼量明顯增長,須要一種更好的方式。
支持序列執行的Promise
理想的方法---鏈式調用:
promise()
.then(obj.api1)
.then(obj.api2)
.then(obj.api3)
.then(obj.api4)
.then(function(val4) {
},function(err) {
}).done();
複製代碼
經過改造代碼以實現鏈式調用:
var Deferred = function() {
this.promise = new Promise();
}
//完成狀態
Deferred.prototype.resolve = function(obj) {
var promise = this.promise;
var handler;
while((handler = promise.queue.shift())) {
if(handler && handler.fulfilled) {
var ret = handler.fulfilled(obj);
if(ret && ret.isPromise) {
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
}
//失敗狀態
Deferred.prototype.reject = function(err) {
var promise = this.promise;
var handler;
while((handler = promise.queue.shift())) {
if(handler && handler.error) {
var ret = handler.error(err);
if(ret && ret.isPromise) {
ret.queue = promise.queue;
this.promise = ret;
return;
}
}
}
}
//生成回調函數
Deferred.prototype.callback = function() {
var that = this;
return function(err, file) {
if(err) {
return that.reject(err);
}else {
that.resolve(file);
}
}
}
var Promise = function() {
this.queue = [];
this.isPromise = true;
}
Promise.prototype.then = function(fulfilledHandler, errorHandler, progressHandler) {
var handler = {};
if(typeof fulfilledHandler === 'function') {
handler.fulfilled = fulfilledHandler;
}
if(typeof errorHandler === 'function') {
handler.errorHandler = errorHandler;
}
this.queue.push(handler);
return this;
}
複製代碼
以兩次文件讀取爲例,假設讀取第二個文件是依賴第一個文件中的內容:
var readFile1 = function(file, encoding) {
var deferred = new Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
}
var readFile2 = function(file, encoding) {
var deferred = new Deferred();
fs.readFile(file, encoding, deferred.callback());
return deferred.promise;
}
readFile1('file1.txt', 'utf8').then(function(file1) {
return readFile2(file1.trim(), 'utf8');
}).then(function(file2) {
//file2
})
複製代碼
要讓Promise支持鏈式執行,主要經過兩個步驟。
//smooth(fs.readFile)
var smooth = function(method) {
return function() {
var deferred = new Deferred();
var ags = Array.prototype.slice.call(argument, 1);
args.push(deferred.callback());
method.apply(null, args);
return deferred.promise;
}
}
複製代碼
上面的兩次讀取文件能夠簡化爲:
var readFile = smooth(fs.readFile);
readFile('file1.txt', 'utf8').then(function(file1) {
return readFile(file1.trim(), 'utf8');
}).then(function(file2) {
})
複製代碼
書中沒有提到這種模式,下面補充一下。
Generator函數是ES6提供的一個異步解決方案。最大的特色是能夠暫停執行。 Generator函數和普通的函數區別有兩個, 1:function和函數名之間有一個*號, 2:函數體內部使用了yield表達式
調用 Generator 函數後,該函數並不執行,返回的也不是函數運行結果,而是一個指向內部狀態的指針對象(Iterator Object)須要調用遍歷器的next()方法才能使函數繼續執行,直到遇到yield方法再次暫停執行。
function* gen() {
var a = 10;
console.log(a);
yield a ++;
console.log(a);
}
複製代碼
上面是一個Generator函數,運行:
var g = gen();
複製代碼
並麼有打印出a的值,執行gen()後只是獲得了一個遍歷器對象。
g.next();
//10
複製代碼
執行遍歷器的next()方法後輸出了10。
g.next();
//11
複製代碼
再次執行next(),yield後的語句被執行輸出10。
遇到yield表達式,就暫停執行後面的操做,並將緊跟在yield後面的那個表達式的值,做爲返回的對象的value屬性值
function *readFileStep(path1) {
let path2 = yield new Promise((resovle, reject) => {
fs.readFile(path1, 'utf8', (err, data) => {
resovle(data);
});
});
let path3 = yield new Promise((resovle, reject) => {
fs.readFile(path2, 'utf8', (err, data) => {
resovle(data);
});
});
return new Promise((resovle, reject) => {
fs.readFile(path3, 'utf8', (err, data) => {
resovle(data);
});
});
}
function run(it) {
function go(result) {
if (result.done) {
return result.value;
}
return result.value.then(function(value) {
return go(it.next(value));
});
}
return go(it.next());
};
run(readFileStep('./file1.txt')).then((data) => {
console.log(data)
});
複製代碼
async函數是對Generator函數的改進,在ES7中出現。Generator函數須要依靠執行器才能執行,async函數自帶執行器執行方法與常規函數同樣。
與Generator函數同樣在異步操做的時候async函數返回一個Promise對象,使用then()方法進行後續處理。
使用async實現Generator函數中的樣例:
async function readFileStep(path1) {
let path2 = await new Promise((resovle, reject) => {
fs.readFile(path1, 'utf8', (err, data) => {
resovle(data);
});
});
let path3 = await new Promise((resovle, reject) => {
fs.readFile(path2, 'utf8', (err, data) => {
resovle(data);
});
});
return new Promise((resovle, reject) => {
fs.readFile(path3, 'utf8', (err, data) => {
resovle(data);
});
});
}
readFileStep('./file1.txt').then((data) => {
console.log(data)
});
複製代碼
只須要像普通函數同樣執行readFileStep便可獲得最終結果的Promise對象。