玩node的同志們都知道,當這門語言被提出來的時候,作為自己最為驕傲的異步機制,卻被PHP和Python等戰團噴得不成樣子的是,他們嘲笑着nodejs那蠢蠢的無限嵌套,nodejs戰團只能以我們只要性能!!!
來安慰自己。
眾所周知,javascript作為一個單線程語言,所有工作都是阻塞的,有好多人不理解為什么說是javascript是阻塞
的,怎么可以做到異步機制呢?
舉一個栗子
在我們平時可以接觸到的情況下,我們可以用瀏覽器來觸發XMLHttpRequest(Ajax)來異步獲取數據,setTimeout、setInterval來完成定時任務,而這並不是javascript的語言來決定這些異步操作的,而是解釋Javascript的瀏覽器來去操作線程作多線程操作的,可以把這些方法理解為瀏覽器拋出的多線程API。而nodejs是基於高性能v8來實現,它也是像瀏覽器一樣,拋出了很多操作線程的API,從而來實現異步機制
。
異步的機制可以讓我們更為節省系統資源,並不需要為每一個請求去像PHP,Tomcat一樣新開一個線程,node內部會有處理各種任務的線程(使用Net,File System,Timers 等很多模塊來操作不同的線程),把不同的異步任務分發給各個任務線程,並會彈性地為線程分配硬件,這都是來自v8的高性能,也是為什么nodejs能面對高I/O情況的根本原因。
現實
到頭來我們必須面對血淋淋的現實,當我初接觸node的時候,代碼也是這樣寫的
fs.readFile(MrFileFirst,"utf8",function(err,data1){
if(err){
//do err thing
}else{
fs.readFile(MrFileSecond,"utf8",function(err,data2){
if(err){
//do err thing
}else{
mongo.find(SomeQuery,function(err,data3){
if(err){
//do err thing
}else{
//do the real thing with [data1,data2,data3]
}
})
}
})
}
})
Oh,my god!好好的異步機制還是玩成了同步……而且慘不忍睹!僅僅只是想返回最后的三個數據,但是這個例子三個任務之間並沒有關系嵌套,這樣子強行把異步玩成同步的話,還是阻塞的代碼,這段代碼的工作時序大概在這樣的:
和不用node並沒有什么區別,完全是阻塞的。在平時我們可以碰到更多的關系層級的嵌套(下一步的操作要基於上一步的結果),這時才必須使用同步去完成任務,但是要是像上面這樣寫的話,我相信你會寫到吐血的(我已經忘了我在代碼中寫過多個少if (err) {}
了,因為node的底層API異步方法都是以err為第一個參數,使得上層所有異步方法都為這種模式)
進化
有人看不下去了,便自會有人站出來,我們漸漸地實現了從無到有的過程,我最開始接觸的是阿里的
eventproxy:
var ep = require("eventproxy");
ep.create("task1","task2","task3",function(result1,result2,result3){
//do the real thing with [result1,result2,result3]
}).fail(function(e){
//do err thing
});
fs.readFile(MrFileFirst,"utf8",ep.done("task1"));
fs.readFile(MrFileSecond,"utf8",ep.done("task2"));
fs.readFile(MrFileThird,"utf8",ep.done("task3"));
這樣,就可以實現三個文件異步進行讀取,並且在三個任務都完成時進行最終的工作,時序圖如下圖:
三個任務幾乎同時觸發(除去代碼的觸發時間),所以左邊的三個點其實可以看作是一個點,而這三個任務都去同時異步進行
,在三個任務都完成的時候,來觸發最后的任務。
這才是node發揮出自己優點的地方,處理時間節省了很多(如果三個任務的時間消耗都為1,則時間縮減了2/3),這才是大node.js。
eventproxy也有更多的用法,可以去其npm上看看。
async
async是國外強大的異步模塊,它的功能與eventproxy相似,但是維護速度與周期特別快,畢竟是用的人多呀,但是支持國產——是一種情懷,附介紹使用async的文章
http://blog.fens.me/nodejs-async/
再進化
人總是不知足的,而剛好是這個不知足,才讓我們不停地去探索想要的、更為方便的東西。而這時,便有人想讓自己寫的代碼復用性更高,同時也不想去寫那么多的callback去嵌套,這時便有了Promiss/A+規范,其是:
An open standard for sound, interoperable JavaScript promises—by implementers, for implementers.
一個健全的通用JavaScript Promise開放標准,源於開發者,並歸於開發者
在ES6中也新增了原生Promise
的使用,而之前Promise庫有promise,Q,bluebird等,在這些庫中現在已經慢慢對ES6的原生Promise作了兼容,雖然ES6現在還沒有大規模投入使用過程中。
在其中最為出名的則是bluebird和Q庫,我使用的是bluebird,先貼一段bluebird的使用代碼感受感受
bluebird:
//CAST
//MrFileOne.txt
//MrFileTow.txt
//MrFileThree.txt
//關系嵌套任務
var Promise = require("bluebird"),
readFileAsync = Promise.promisify(require("fs").readFile);
readFileAsync("MrFileOne.txt","utf8")
.then(function(data){
//if the data contains the path of MrFileTow
var path = ..... //do something with data
return readFileAsync(path,"utf8");
})
.then(function(data){
//if the data contains the path of MrFileThree
var path = ..... //do something with data
return readFileAsync(path,"utf8");
})
.then(function(data){
//get the data of MrFileThree
//do something
})
.catch(function(err){
console.log(err);
});
//無關系匯總任務
Promise.all([
readFileAsync("MrFileOne.txt","utf8"),
readFileAsync("MrFileTwo.txt","utf8"),
readFileAsync("MrFileThree.txt","utf8")
])
.then(function(datas){
//do something with three data form our actors
})
.catch(function(err){
console.log(err);
});
有沒有一下被這種寫法所吸引,這就是Promise模塊的魅力,它很優雅地將函數的回調寫在了then里面,並為then返回一個新的Promise,以供下一次的then去回調本次返回的結果。
How
首先使用了方法:
readFileAsync = Promise.promisify(rquire("fs").readFile);
這個方法則是為復制了readFile方法並為其增添了Promise
機制,而Promise機制是什么呢?那就是為其添加Promise方法和屬性后,讓整個方法的返回值為一個Promise對象,我們可以通過Promise來調用then
方法,來去對這個Promise方法的回調進行處理。在Promise中都默認的將第一個參數err放在了后面的catch
中,使得我們再也不用寫那么多的if(err)
了。我們可以直接通過在then方法中通過函數參數來獲取這個Promise的異步數據,從而進行下一步的處理。
而在then方法后,其返回的也是一個Promise對象,我們可以在其后再次進行then來獲取上一個then的數據並進行處理。當然,我們也可以人為地去決定這個then的返回參數,但是整個then方法返回的都是一個Promise對象。
readFileAsync("MrFileOne.txt","utf8")
.then(function(data){
if(.....){ //data isn't what we want
Promise.reject("It's not correct data!");
}else{
return data;
}
})
.then(function(){
console.log("yeah! we got data!");
})
.catch(function(err){
console.log(err);
})
在上面代碼中,如果獲取到的data並不是我們想要的,則我們可直接調用Promise.reject
拋出一個ERROR
,並直接交給catch
來處理錯誤,所以在控制台我們能得到的是“It's not correct data!”,並不會得到“yeah! we got data!”,因為拋出錯誤后其之后的then方法並不會跟着執行。
More
當然我們也可以自定義多個catch來捕獲不同的ERROR,對其作不同的處理,就像下面的一樣
var customError = new Error(SOMENUMBER,SOMEDESCRIPTION)
readFileAsync("MrFileOne.txt","utf8")
.then(function(data){
switch(data){
case CASE1:
Promise.reject(customError);
case CASE2:
Promise.reject(new SyntaxError("noooooo!"));
}
})
.catch(customError,function(err){
//do with customError
})
.catch(SyntaxError,function(err){
//do with SyntaxError
})
.catch(function(err){
console.log(err);
})
而更多的使用方法,可以在bluebird on npm里學習得到,相信你看了之后會愛上Promise的。
Q
Q模塊也是一個非常優秀的Promise,它的實現原理和bluebird都大同小異,都是基於Promise/A+標准來擴展的,所以使用上甚至都是差不了多少的,選擇哪一個就看個人愛好了。
Promise編程思想
重點來啦,我們先來看一段普通的代碼
var obj = (function(){
var variable;
return {
get: function(){
return variable;
},
set: function(v){
variable = v;
}
}
})();
exports.get = obj.get;
exports.set = obj.set;
這個代碼實現的是創建了一個閉包來儲存變量,那么我在外部調用這個模塊時,則可以去操作這個值,即實現了一個Scope變量,並把它封裝了起來。
矛盾
根據我們以前的思想,這段代碼看起來很正常,但是這時侯我要加一判斷進去,即在get
方法調用時,如果varibale
為undefined
,那么我則去做一個讀文件的操作,從文件中將它讀出來,並反回,你會怎么實現呢?
你會發現,通過以往的思維,你是無法做到這一方法的,那么使用異步思維去想想呢,好像有點門頭:
get: function(callback){
if(varibale){
callback(varibale);
}else{
fs.readFile("SomeFile","utf8",function(err,data){
if(err){
//do with err
return;
}
callback(data);
})
}
}
這樣……嗯咳咳,看起來似乎好像也許解決的還可以,但是你自己也會覺得,這其實糟透了,我們將原本的簡單get函數更改得這么復雜。那么問題來了,誰會在使用的時候會想到這個get方法其實是一個回調的方法呢?你平時使用get時你會考慮說是這個里面有可以是回調嗎?我們都是直接get()
來獲取它的返回值。
這就是我們自己給自己造成的矛盾和麻煩,這也是我以前曾經遇到的。
突破
那么在模塊化的node里,我們怎么去實現這些不必要的麻煩呢?那就是用Promise思想去編寫自己的代碼,我們先試着用上面說到的bluebird來加工一下這段代碼:
var Promise = require("bluebird"),
fs = require("fs");
var obj = (function(){
var variable;
return {
get: function(){
if(variable){
return Promise.resolve(variable);
}
return Promise.promisify(fs.readFile)("SomeFile","utf8");
},
set: function(v){
return Promise.resolve(variable = v);
}
}
});
exports.get = obj.get;
exports.set = obj.set;
就是這么漂亮,使用Promise.resolve
方法則是將變量轉化為一個Promise對象,則是我們在外部對這個模塊進行使用時,則要求我們使用Promise的思想去應用模塊拋出的接口,比如:
var module = require("thisModule.js");
module.get()
.then(function(data){
console.log(data);
module.set("new String");
return module.get;
})
.then(function(data){
console.log(data);
});
當我們使用Promise思想去面對每一個接口的時候,我們可以完全不用考慮這個模塊的代碼是怎么寫的,這個方法該怎么用才是對的,到底是回調還是賦值。我們可以很直接的在其模塊方法后then
來解決一切問題!不用關心前面的工作到底做了什么,怎么做的,到底是異步還是同步,只要我們將整個工作流程都使用Promise來做的話,那會輕松很多,而且代碼的可讀性會變得更好!
尼瑪!簡直是神器啊!
使用Promise編程思想去和node玩耍,你會相信真愛就在眼前。同時我也相信在前端模塊化加速的今天,Promise編程思想必定會滲透至前端的更多角落。
Finish.