函數式編程
函數式編程是異步編程的基礎,在JS中,將函數作爲參數,返回值,都是可以的。這爲我們使用回調函數打下了很好的基礎。
var points = [40, 100, 1, 5, 25, 10];
points.sort(function(a, b)
{
return a - b;
}); // [ 1, 5, 10, 25, 40, 100 ]
var isType = function (type) {
return function (obj) {
return toString.call(obj) == '[object ' + type + ']';
};
};
var isString = isType('String');
var isFunction = isType('Function');
難點
異常處理
由於異步,try/catch這樣的代碼不會起到任何作用。
所以一般對回調函數都要求第一個參數接收錯誤信息。
在我們自行編寫的異步方法上,也要遵循這個原則,在調用傳進來的回調時,第一個參數傳異常。
我們的異步方法:
var async = function (callback) {
process.nextTick(function() {
var results = something;
if (error) {
return callback(error);
}
callback(null, results);
});
};
函數嵌套過深
這個在各種有回調機制的編程中都是個問題,比如iOS。
多線程編程
由於JS執行在單線程,Node實際上沒有充分利用多核CPU的性能。在瀏覽器端存在同樣的問題,因此出現了Web Worker來將於UI無關的計算任務交給其他線程。
Node借鑑了這一點,使用child_process和cluster模塊來完成這些。
異步編程解決方案
主要有3種解決方案:
- 事件發佈/訂閱模式
- Promise/Deferred模式
- 流程控制庫
事件發佈/訂閱模式
這個是用途最廣的異步方式,操作也很簡單,這裏我們使用Node自帶的events模塊。
首先你訂閱一個事件,並定義在這個事件發生時你會做什麼的回調函數。
然後在事件發生時你觸發它。
var emitter = require('events');
var myEmitter = new emitter();
myEmitter.on("event1", (message1,message2) => {
console.log(message1);
console.log(message2);
}); // 發佈
myEmitter.emit('event1', "I am message1!", "I am message2!");
這是一個很好的解耦邏輯的方式。也是一個很好的封裝的方式,只將像暴露的過程和信息通過事件發佈的方式告訴外面,外面想使用就只能通過訂閱的方式。
繼承events
Node中近半數的模塊都繼承自EventEmitter類,這樣方便使用事件。
const EventEmitter = require('events');
class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('an event occurred!');
});
myEmitter.emit('event');
或者老一點的辦法:
//繼承EventEmitter模塊,使得自己的類也可以發佈事件
var util = require('util');
function Stream() {
emitter.EventEmitter.call(this);
}
util.inherits(Stream, emitter.EventEmitter);
var myStream = new Stream();
myStream.on("event1", (message) => {
console.log(message);
}); // 發佈
myStream.emit('event1', "I am stream!");
利用事件隊列解決雪崩問題
比如對於一次數據庫的查詢:
var select = function (callback) {
db.select("SQL", function (results) {
callback(results);
});
};
這個數據庫的查詢在沒有緩存硬執行時,當請求數量非常大時是對性能影響非常大的,它們查詢的是同一個語句,我們只要保證給他們的數據是最新的就行。
這時我們可以使用狀態鎖:
var status = "ready";
var select = function (callback) {
if (status === "ready") {
status = "pending";
db.select("SQL", function (results) {
status = "ready";
callback(results);
});
}
};
但是在這種情況下,連續多次的調用select()時,第前一次完成前,後面的是不會被執行的。
這時加入事件隊列就很不錯,通過once添加的監聽器只能執行一次,在執行後就會將它與關聯的事件移除:
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 (results) {
proxy.emit("selected", results);
status = "ready";
});
}
};
每一次select()被調用時,不管上一次是否執行完,我們都把它壓入到一個事件隊列中。此時如果上次查詢已經執行完了,那麼這次查詢會立即執行;如果沒有執行完,那麼這次查詢會像上面一樣不執行,但是這裏不同的是,這個查詢的請求沒有被忽略,而是在事件隊列中等待。
在一個查詢執行完並返回結果的時候,這個結果會傳給在隊列中等待的所有查詢請求的回調。這就意味着在這次查詢結果產生前進來的所有的查詢請求就都得到了結果。
多異步之間的協作方案
有時需要等待多個事件完成才能繼續下去,這多個事件很可能沒有順序,沒有關聯。這時事件與監聽器的關係其實是多對一的,最簡單直觀的辦法是使用多層嵌套,一個完成了再執行另一個,可是這樣無論從效率還是代碼上都是不好的。我們可以使用哨兵變量:
var count = 0;
var results = {};
var done = function (key, value) {
results[key] = value;
count++;
if (count === 3) {
render(results);
}
};
fs.readFile(template_path, "utf8", function (err, template) {
done("template", template);
});
db.query(sql, function (err, data) {
done("data", data);
});
l10n.get(function (err, resources) {
done("resources", resources);
});
這樣就可以同步的執行它們了。
如果你想要可定製次數的done函數,這裏利用了閉包來保存count的狀態:
var after = function (times, callback) {
var count = 0, results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
};
};
var done = after(times, render);
通過簡單的拓展,多對多的形式也可以實現:
var after = function (times, callback) {
var count = 0, results = {};
return function (key, value) {
results[key] = value;
count++;
if (count === times) {
callback(results);
}
};
};
var done = after(3, render);
var emitter = new events.Emitter();
emitter.on("done", done);
emitter.on("done", other);
fs.readFile(template_path, "utf8", function (err, template) {
emitter.emit("done", "template", template);
});
db.query(sql, function (err, data) {
emitter.emit("done", "data", data);
});
l10n.get(function (err, resources) {
emitter.emit("done", "resources", resources);
});
這樣在done事件發生時可以同時觸發other回調。
Promise/Deferred模式
這個模式允許你在不完全設置好回調的情況下進行異步調用。使用起來更加靈活。
最後的調用看起來是這樣的,我們使用jq來舉個例子:
$.when(d)
.done(function(){ alert("哈哈,成功了!"); })
.fail(function(){ alert("出錯啦!"); });
這樣是不是更加方便易讀了,而且此時就算你不指定done和fail的回調,d這個函數也會照樣完成它的工作。如果你想有多個回調,由於實現了鏈式調用,繼續在後麪點就好了。
我們在node裏以event爲基礎來實現它:
//首先promise繼承event,我們將在promise裏完成事件的訂閱和發佈
//這裏定義了3個方法,done,fail和它們的組合then
//done和fail都使用once來綁定事件,保證其只執行一次
//新定義的這三個方法完成了事件的訂閱,並且都返回了自己以完成鏈式調用
var Promise = function () {
emitter.EventEmitter.call(this);
};
util.inherits(Promise, emitter.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;
};
Promise.prototype.done = function (fulfilledHandler) {
if (typeof fulfilledHandler === 'function') {
this.once('success', fulfilledHandler);
}
return this;
};
Promise.prototype.fail = function (errorHandler) {
if (typeof errorHandler === 'function') {
this.once('error', errorHandler);
}
return this;
};
可以看到,這裏這三個新的自定義方法的作用就是將各個處理函數存起來,這裏整個事件有3個狀態:處理中,完成和失敗。我們還需要一個對象來觸發事件狀態的轉變:
//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 (err) {
this.state = 'failed';
this.promise.emit('error', err);
};
Deferred.prototype.progress = function (data) {
this.promise.emit('progress', data);
};
啊,這樣我們真正的業務邏輯就可以直接調用這幾個方法來觸發各個狀態了。
爲了通用我們可以做一個簡單的工具函數:
var when = function (func) {
var defrred = new Deferred();
func(defrred);
return defrred.promise;
};
我們真正的業務邏輯就是func了,我們把defrred對象傳給我們的業務邏輯,業務邏輯就可以根據自己的需要來觸發事件了。我們這裏只返回defrred.promise,不把整個defrred暴露給外面,這樣外面就不能隨便調用defrred來轉變事件的狀態了。
我們假設一個業務邏輯:
function eat(dfd) {
console.log('give me apple or beef');
var tasks = function(){
if (food=='apple') {
console.log('you give me apple');
dfd.reject();
}
if (food=='beef') {
console.log('you give me beef');
dfd.resolve();
}
};
setTimeout(tasks,5000);
}
這裏使用settimeout來模擬異步。
調用:
when(eat)
.done(function(){ console.log('I eat beef'); })
.fail(function(){ console.log('I throw apple'); });
這裏就算不給done和fail,eat還是會正常執行,就是會觸發沒有的事件而已。
多異步協作
這裏就又要提到一對多,多對一,多對多。
一對多我們可以直接實現:
when(eat)
.done(function(){ console.log('I eat beef'); })
.done(function(){ console.log('I eat beefaaa'); })
.fail(function(){ console.log('I throw apple'); });
多對一我們就需要新的方法了:
Deferred.prototype.all = function (promises) {
var count = promises.length;
var that = this;
var results = [];
promises.forEach(function (promise, i) {
promise.then(function (data) {
count--;
results[i] = data;
if (count === 0) {
that.resolve(results);
}
}, function (err) {
that.reject(err);
});
});
return this.promise;
};
在每一個異步事件執行成功檢查一下是否所有的都執行完了,都執行完了就調用頂上的全部執行完的方法,有一個fail了就調用頂上的fail方法。
使用after方法封裝一下:
var after = function(promises) {
var defrred = new Deferred();
return defrred.all(promises);
}
使用:
after([when(eat),when(drink)])
.done(function(){ console.log('I eat all'); })
.fail(function(){ console.log('I don\'t like one of those'); })
流程控制庫
這種方式並不是規範中主流的方式,但是相應的也更加的靈活。
我們來看看最流行的流程控制模塊async。
異步的串行
var async = require("async");
var fs = require("fs");
async.series([function (callback) {
console.log("reading 1");
fs.readFile('file1.txt', 'utf-8', callback);
}, function (callback) {
//fs.readFile('file2.txt', 'utf-8', callback);
console.log("reading 2");
callback("2222222","33333");
}], function (err, results) {
// results => [file1.txt, file2.txt]
console.log("results:"+results);
});
這個series方法會依次執行數組裏的函數,這裏的callback是由async通過高階函數的方式注入,每一個函數裏會有callback來接收這個函數要返回的結果,無論是同步的還是異步的,無論執行的快或慢,series都會按照順序來執行這些函數並將這些結果存在數組中,在所有函數執行完之後,這個數組就可以使用了。
異步的並行執行
async.parallel([ function (callback) {
fs.readFile('file1.txt', 'utf-8', callback);
},
function (callback) {
fs.readFile('file2.txt', 'utf-8', callback);
} ],
function (err, results) { // results => [file1.txt, file2.txt]
console.log("results:"+results);
});
異步調用的依賴處理
當前一個結果是後一個調用的輸入時,問題就不能用series來解決了。
async.waterfall([ function (callback) {
//file1.txt裏下一個文件的地址
fs.readFile('file1.txt', 'utf-8', function (err, content) { callback(err, content); });
}, function (arg1, callback) { // arg1 => file2.txt
fs.readFile(arg1, 'utf-8', function (err, content) { callback(err, content); });
}], function (err, result) {
// result => file2.txt
console.log("results:"+result);
});
自動依賴處理
這裏就是async強大的地方了,你給他一個你所有要做的事情的對象,以及每個事情依賴誰,它會自動幫你判斷該怎麼執行:
var deps = {
readConfig: function (callback) {
console.log("// read config file");
console.log(callback);
},
connectMongoDB: ['readConfig', function (callback) {
console.log("// connect to mongodb");
console.log(callback);
}],
connectRedis: ['readConfig', function (callback) {
console.log("// connect to redis ");
//callback();
}],
complieAsserts: function (callback) {
console.log("// complie asserts");
//callback();
},
uploadAsserts: ['complieAsserts', function (callback) {
console.log("// upload to assert");
//callback();
}],
startup: ['connectMongoDB', 'connectRedis', 'uploadAsserts', function (callback) {
console.log("// startup ");
}]
};
async.auto(deps);
其他庫
相應的還有其他可用的流程控制庫,這裏就不一一介紹了,比較有代表的是Step和Wind
異步併發控制
因爲是異步的,所以當請求多的時候很可能給系統的低層服務帶來很大的壓力甚至直接崩潰。比如打開文件這個操作,以前同步的時候同時打開的文件不會太多,都是完成一個再去打開另一個。但是異步時可能一下打開了大量的文件,從而引發問題。
所以我們應該對異步調用的數量做一定的限制。
在async中專門有一個方法來有限制的執行異步調用:
async.parallelLimit([ function (callback) {
fs.readFile('file1.txt', 'utf-8', callback);
}, function (callback) {
fs.readFile('file2.txt', 'utf-8', callback);
} ], 1, function (err, results) {
console.log("resultsLimit:"+results);
});