前言
JS需要異步處理的地方實在是比較多羹奉,比如定時器/ajax/io操作等等,在當今前端技術日新月異的情況下约计,異步編程成了核心技能之一诀拭,在這里我只是羅列一下幾種我用過的異步編程方式并稍加對比。本次編寫的代碼全部在node 7+版本中運行
同步和異步
首先我們要弄清同步和異步到底是個什么玩意兒煤蚌,其實我的理解就是他們對代碼的“執(zhí)行順序”控制程度不一樣耕挨。為什么這樣說呢?因為同步在一段代碼調用之后尉桩,是不管有沒有結果返回的筒占,立馬就執(zhí)行到下一步去了。而異步蜘犁,是會等待那個調用的赋铝,直到返回了結果再往下執(zhí)行。
舉個例子:假設有個搶紅包的調用,它是需要一段時間才能滿足搶紅包結束的
var result = function(){
if(搶紅包結束) return 5
}
console.log(result())
如果是同步沽瘦,這段代碼就不管result的死活了直接往下走革骨,輸出undefined,如果寫成異步風格的代碼析恋,那就不一樣了良哲。
回調函數(shù)
在前端的遠古時代,回調是處理異步的不二選擇助隧,為什么筑凫,因為它的寫法簡單滑沧,沒有多余的api。就拿剛剛那個搶紅包的例子來說巍实,我用一個定時器替代它:
var result = function(){
setTimeout(()=>{
return 5;
},1000)
}
console.log(result())
用回調函數(shù)處理怎么弄呢滓技?很簡單,讓result的參數(shù)為一個回調函數(shù)就可以了,于是代碼變成下面這樣
var result = function(callback){
setTimeout(()=>{
callback(5)
},1000)
}
result(console.log)
現(xiàn)在我們用一個真實的io調用替代搶紅包棚潦,新建一個numbers.txt令漂,在里面寫若干個紅包金額,代碼如下:
const fs = require('fs');
const readFileAsArray = function (file, cb) {
fs.readFile(file, (err, data) => {
if (err) return cb(err);
const lines = data.toString().trim().split('\n');
cb(null, lines);
})
}
readFileAsArray('./numbers.txt', (err, lines) => {
if (err) throw err;
const numbers = lines.map(Number);
console.log(`分別搶到了${numbers}塊紅包`);
})
代碼輸出為:
>分別搶到了10,11,12,13,14,15塊紅包
從代碼中我們可以看到,定義了一個readFileAsArray函數(shù)丸边,傳兩個參:文件名和回調函數(shù)叠必,然后調用這個函數(shù),把回調函數(shù)寫入第二個參數(shù)里妹窖,就可以控制代碼執(zhí)行順序了纬朝。
不過,回調的缺點就是寫多了骄呼,層層嵌套共苛,又會造成回調地獄的坑爹情況,代碼變得難以維護和閱讀蜓萄。所以我們需要更好的解決辦法隅茎。
Promise
借用ydjs的一句話:Promise實現(xiàn)了控制反轉。什么意思呢绕德?原來這個順序的控制是在代碼那邊而不是程序員控制,現(xiàn)在有了Promise摊阀,控制權就由人來掌握了耻蛇,通過一系列Promise的方法如then/catch/all/race等控制異步流程。<a >Promise文檔</a>
還是剛剛那個搶紅包的例子胞此,這次用Promise來寫就是這樣的:
const fs = require('fs');
const readFileAsArray = function (file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
}
const lines = data.toString().split('\n');
resolve(lines);
})
})
}
readFileAsArray('./numbers.txt').then(
lines => {
const numbers = lines.map(Number);
console.log(`分別搶到了${numbers}塊紅包`);
}
).catch(error => console.error(error));
結果和使用回調函數(shù)一樣臣咖,但是在這里已經把控制權交給了程序員,代碼也變得更好理解漱牵。雖然Promise有單值/不可取消等缺點夺蛇,不過在現(xiàn)在大部分的情況下實現(xiàn)異步還是夠用的。想深入了解的朋友可以去看看《你不知道的JS》中卷第三章酣胀。
await/async
Promise的api太多了刁赦,有沒有簡化的辦法呢?答案是肯定有的闻镶,ES7推出了一個語法糖:await/async甚脉,它的內部封裝了Promise和Generator的組合使用方式,至于Generator是什么铆农,這里不再贅述牺氨,有興趣的朋友們可以去自行研究。
于是,剛剛那段代碼就變成了:
const fs = require('fs');
const readFileAsArray = function (file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
}
const lines = data.toString().split('\n');
resolve(lines);
})
})
}
async function result() {
try {
const lines = await readFileAsArray('./numbers.txt');
const numbers = lines.map(Number);
console.log(`分別搶到了${numbers}塊紅包`);
} catch (err) {
console.log("await出錯猴凹!");
console.log(err);
}
}
result();
這樣做的結果是不是讓代碼可讀性更高了夷狰!而且也屏蔽了Promise和Generator的細節(jié)。
event
另一個實現(xiàn)異步的方式是event郊霎,回調(promise沼头、await/async)和event的關系就像計劃經濟和市場經濟一樣,一個是人為的強制性的控制歹篓,一個是根據需求和供給這只看不見的手控制瘫证。
還是同一個例子,用event寫就是這樣:
const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
executeAsy(asyncFunc, args) {
this.emit("開始");
console.time('執(zhí)行耗時');
asyncFunc(args, (err, data) => {
if (err) return this.emit('error', err);
this.emit('data', data);
console.timeEnd('執(zhí)行耗時');
this.emit("結束");
});
}
}
const myEventEmitter = new MyEventEmitter();
myEventEmitter.on('開始', () => {
console.log('開始執(zhí)行了');
})
myEventEmitter.on('data', (data) => {
console.log(`分別搶到了${data}塊紅包`);
})
myEventEmitter.on('結束', () => {
console.log('結束執(zhí)行了');
})
myEventEmitter.on('error', (err) => {
console.error(err);
})
myEventEmitter.executeAsy(fs.readFile, './numbers.txt');
這種事件驅動非常靈活庄撮,也不刻意去控制代碼的順序背捌,一旦有事件的供給(emit),它就會立刻消費事件(on)洞斯,不過正是因為這樣毡庆,它的缺點也很明顯:讓程序的執(zhí)行流程很不清晰。
event+promise+await/async
純粹的計劃經濟也不好烙如,純粹的市場經濟也不好么抗。好的方式是什么?當然是結合起來啦亚铁!
所以就有了結合event和promise的寫法:
const EventEmitter = require('events');
const fs = require('fs');
class MyEventEmitter extends EventEmitter {
async executeAsy(asyncFunc, args) {
this.emit("開始");
try {
console.time('執(zhí)行耗時');
const data = await asyncFunc(args);
this.emit('data', data);
console.timeEnd('執(zhí)行耗時');
this.emit('結束');
} catch (err) {
console.log("出錯了!");
this.emit('error', err);
}
}
}
const readFileAsArray = function (file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
}
const lines = data.toString().split('\r\n');
resolve(lines);
})
})
}
const myEventEmitter = new MyEventEmitter();
myEventEmitter.on('開始', () => {
console.log('開始執(zhí)行了');
})
myEventEmitter.on('data', (data) => {
console.log(`分別搶到了${data}塊紅包`);
})
myEventEmitter.on('結束', () => {
console.log('結束執(zhí)行了');
})
myEventEmitter.on('error', (err) => {
console.error(err);
})
myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');
這種結合的方式基本上可以應付現(xiàn)今的異步場景了蝇刀,缺點嘛。徘溢。吞琐。就是代碼量比較多
rxjs
js越發(fā)壯大,jser們終于站起來了然爆,看著其他語言使用著rx這個強大的工具站粟,我們怎么能少,一種大一統(tǒng)管理異步的方案:rxjs就這樣來到了世上曾雕。
簡單介紹下rxjs和異步的關系:它可以把數(shù)據轉化成一股流奴烙,無論這個數(shù)據是同步得到的還是異步得到的,是單值還是多值剖张。
比如用Rx.Observable.of來包裝單值同步數(shù)據切诀,
用Rx.Observable.of來包裝單值同步數(shù)據,
用Rx.Observable.fromPromise來包裝單值異步數(shù)據搔弄,
以及用Rx.Observable.fromEvent來包裝多值異步數(shù)據:
const fs = require('fs');
const Rx = require('rxjs');
const EventEmitter = require('events');
class MyEventEmitter extends EventEmitter {
async executeAsy(asyncFunc, args) {
this.emit("開始");
try {
console.time('執(zhí)行耗時');
const data = await asyncFunc(args);
this.emit('data', data);
console.timeEnd('執(zhí)行耗時');
this.emit('結束');
} catch (err) {
console.log("出錯了!");
this.emit('error', err);
}
}
}
const readFileAsArray = function (file) {
return new Promise((resolve, reject) => {
fs.readFile(file, (err, data) => {
if (err) {
reject(err);
}
const lines = data.toString().split('\r\n');
resolve(lines);
})
})
}
const myEventEmitter = new MyEventEmitter();
myEventEmitter.executeAsy(readFileAsArray, './numbers.txt');
let dataObservable = Rx.Observable.fromEvent(myEventEmitter, 'data')
let subscription = dataObservable.subscribe((data) => {
console.log(`分別搶到了${data}塊紅包`);
}, err => {
console.error(err);
}, compelete => {
console.info("compelete!");
})
rxjs還有很多重要的概念趾牧,比如生產者Observe和消費者Observable、推拉模型肯污、各種方便的操作符和函數(shù)式編程等等
關于異步的未來展望
ES8已經著手Observable和Observe的實現(xiàn)了翘单,node也在著手異步生命周期鉤子Async Hooks來方便程序們來調試異步程序吨枉,我相信,未來js的異步編程會變得越來越容易哄芜,功能也會越來越強大~