回調(diào)之痛
每一位前端工程師上輩子都是折翼的天使。
相信很多前端工程師都同我一樣,初次接觸到前端時(shí),了解了些許 HTML、CSS、JS 知識,便驚嘆于前端的美好,沉醉于這種所見即所得的成就感之中。但很快我就發(fā)現(xiàn),前端并沒有想象中的那么美好,JS 也并不是彈一個 alert 這么簡單。尤其是當(dāng)我想這么干,卻發(fā)現(xiàn)無法得到結(jié)果時(shí):
var data = ajax('/url/to/data');
在查閱很多資料后,我知道了 JS 是事件驅(qū)動的,ajax 異步請求是非阻塞的,我封裝的 ajax 函數(shù)無法直接返回服務(wù)器數(shù)據(jù),除非聲明為同步請求(顯然這不是我想要的)。于是我學(xué)會了或者說接受了這樣的事實(shí),并改造了我的 ajax 函數(shù):
ajax('/url/to/data', function(data){
//deal with data
});
在很長一段時(shí)間,我并沒有認(rèn)為這樣的代碼是不優(yōu)雅的,甚至認(rèn)為這就是 JS 區(qū)別于其他語言的特征之一 —— 隨處可見的匿名函數(shù),隨處可見的 calllback 參數(shù)。直到有一天,我發(fā)現(xiàn)代碼里出現(xiàn)了這樣的結(jié)構(gòu):
ajax('/get/data/1', function(data1){
ajax('/get/data/2', function(data2){
ajax('/get/data/3', function(data3){
dealData(data1, data2, data3, function(result){
setTimeout(function(){
ajax('/post/data', result.data, function(ret){
//...
});
}, 1000);
});
});
});
});
這就是著名的回調(diào)金字塔!

在我的理想中,這段代碼應(yīng)該是這樣的:
var data1 = ajax('/get/data/1');
var data2 = ajax('/get/data/2');
var data3 = ajax('/get/data/3');
var result = dealData(data1, data2, data3);
sleep(1000);
var ret = ajax('/post/data', result.data);
//...
承諾的救贖
理想是豐滿的,奈何現(xiàn)實(shí)太骨干。這種回調(diào)之痛在前端人心中是揮之不去的,它使得代碼結(jié)構(gòu)混亂,可讀性變差,維護(hù)困難。在忍受這種一坨坨的代碼很久之后,有一天我偶遇了 Promise,她的優(yōu)雅讓我久久為之贊嘆:世間竟有如此曼妙的異步回調(diào)解決方案。
Promises/A+規(guī)范中對 promise 的解釋是這樣的: promise 表示一個異步操作的最終結(jié)果。與 promise 進(jìn)行交互的主要方式是通過 then 方法,該方法注冊了兩個回調(diào)函數(shù),用于接受 promise 的最終結(jié)果或者 promise 的拒絕原因。一個 Promise 必須處于等待態(tài)(Pending)、兌現(xiàn)態(tài)(Fulfilled)和拒絕態(tài)(Rejected)這三種狀態(tài)中的一種之中。
- 處于等待態(tài)時(shí)
- 可以轉(zhuǎn)移至執(zhí)行態(tài)或拒絕態(tài)
- 處于兌現(xiàn)態(tài)時(shí)
- 不能遷移至其他任何狀態(tài)
- 必須擁有一個不可變的值作為兌現(xiàn)結(jié)果
- 處于拒絕態(tài)時(shí)
- 不能遷移至其他任何狀態(tài)
- 必須擁有一個不可變的值作為拒絕原因
通過 resolve 可以將承諾轉(zhuǎn)化為兌現(xiàn)態(tài),通過 reject 可以將承諾轉(zhuǎn)換為拒絕態(tài)。
關(guān)于 then 方法,它接受兩個參數(shù):
promise.then(onFulfilled, onRejected)
then 方法可以被同一個 promise 調(diào)用多次:
- 當(dāng)
promise成功執(zhí)行時(shí),所有onFulfilled需按照其注冊順序依次回調(diào) - 當(dāng)
promise被拒絕執(zhí)行時(shí),所有的onRejected需按照其注冊順序依次回調(diào)
使用 Promise 后,我的 ajax 函數(shù)使用起來變成了這個樣子:
ajax('/url/to/data')
.then(function(data){
//deal with data
});
看起來和普通的回調(diào)沒什么變化是么?讓我們繼續(xù)研究 then 方法的神奇之處吧。
then 方法的返回值是一個新的 promise:
promise2 = promise1.then(onFulfilled, onRejected);
如果 onFulfilled、onRejected 的返回值 x 是一個 promise,promise2 會根據(jù) x 的狀態(tài)來決定如何處理自己的狀態(tài)。
- 如果 x 處于等待態(tài), promise2 需保持為等待態(tài)直至 x 被兌現(xiàn)或拒絕
- 如果 x 處于兌現(xiàn)態(tài),用相同的值兌現(xiàn) promise2
- 如果 x 處于拒絕態(tài),用相同的值拒絕 promise2
這意味著串聯(lián)異步流程的實(shí)現(xiàn)會變得非常簡單。我試著用 Promise 來改寫所有的異步接口,上面的金字塔代碼便成為這樣的:
when( ajax('/get/data/1'), ajax('/get/data/2'), ajax('/get/data/3') )
.then(dealData)
.then(sleep.bind(null,1000))
.then(function(result){
return ajax('/post/data', result.data);
})
.then(function(ret){
//...
});
一下子被驚艷到了??!回調(diào)嵌套被拉平了,小肚腩不見了!這種鏈?zhǔn)?then 方法的形式,頗有幾分 stream/pipe 的意味。
$.Deferred
jQuery 中很早就有 Promise 的實(shí)現(xiàn),它稱之為 Deferred 對象。使用 jQuery 舉例寫一個 sleep 函數(shù):
function sleep(s){
var d = $.Deferred();
setTimeout(function(){
d.resolve();
}, s);
return d.promise(); //返回 promise 對象防止在外部被別人 resolve
}
我們來使用一下:
sleep(1000)
.then(function(){
console.log('1秒過去了');
})
.then(sleep.bind(null,3000))
.then(function(){
console.log('4秒過去了');
});
jQuery 實(shí)現(xiàn)規(guī)范的 API 之外,還實(shí)現(xiàn)了一對接口:notify/progress。這對接口在某些場合下,簡直太有用了,例如倒計(jì)時(shí)功能。對上述 sleep 函數(shù)改造一下,我們寫一個 countDown 函數(shù):
function countDown(second) {
var d = $.Deferred();
var loop = function(){
if(second <= 0) {
return d.resolve();
}
d.notify(second--);
setTimeout(loop, 1000);
};
loop();
return d.promise();
}
現(xiàn)在我們來使用這個函數(shù),感受一下 Promise 帶來的美好。比如,實(shí)現(xiàn)一個 60 秒后重新獲取驗(yàn)證碼的功能:
var btn = $("#getSMSCodeBtn");
btn.addClass("disabled");
countDown(60)
.progress(function(s){
btn.val(s+'秒后可重新獲取');
})
.then(function(){
btn.val('重新獲取驗(yàn)證碼').removeClass('disabled');
});
簡直驚艷!離絕對的同步編寫非阻塞形式的代碼已經(jīng)很近了!
與 ES6 Generator 碰撞出火花
我深刻感受到,前端技術(shù)發(fā)展是這樣一種狀況: 當(dāng)我們驚嘆于最新技術(shù)標(biāo)準(zhǔn)的美好,感覺一個最好的時(shí)代即將到來時(shí),回到實(shí)際生產(chǎn)環(huán)境,卻發(fā)現(xiàn)一張小小的 png24 透明圖片在 IE6 下還需要前端進(jìn)行特殊處理。但,那又怎樣,IE6 也不能阻擋我們對前端技術(shù)灼熱追求的腳步,說不定哪天那些不支持新標(biāo)準(zhǔn)的瀏覽器就悄然消失了呢?(扯遠(yuǎn)了...)
ES6 標(biāo)準(zhǔn)中最令我驚嘆的是 Generator —— 生成器。顧名思義,它用來生成某些東西。且上例子:

這里我們看到了 function*() 的新語法,還有 yield 關(guān)鍵字和 for/of 循環(huán)。新東西總是能讓人產(chǎn)生振奮的心情,即使現(xiàn)在還不能將之投入使用(如果你需要,其實(shí)可以通過 ES6->ES5 的編譯工具預(yù)處理你的 js 文件)。如果你了解 Python , 這很輕松就能理解。Generator 是一種特殊的 function,在括號前加一個 * 號以區(qū)別。Generator 通過 yield 操作產(chǎn)生返回值,最終生成了一個類似數(shù)組的東西,確切的說,它返回了 Iterator,即迭代器。迭代器可以通過 for/of 循環(huán)來進(jìn)行遍歷,也可以通過 next 方法不斷迭代,直到迭代完畢。

yield 是一個神奇的功能,它類似于 return ,但是和 return 又不盡相同。return 只能在一個函數(shù)中出現(xiàn)一次,yield 卻只能出現(xiàn)在生成器中且可以出現(xiàn)多次。迭代器的 next 方法被調(diào)用時(shí),將觸發(fā)生成器中的代碼執(zhí)行,執(zhí)行到 yield 語句時(shí),會將 yield 后的值帶出到迭代器的 next 方法的返回值中,并保存好運(yùn)行時(shí)環(huán)境,將代碼掛起,直到下一次 next 方法被調(diào)用時(shí)繼續(xù)往下執(zhí)行。
有沒有嗅到異步的味道?外部可以通過 next 方法控制內(nèi)部代碼的執(zhí)行!天然的異步有木有!感受一下這個例子:

還有還有,yield 大法還有一個功能,它不僅可以帶出值到 next 方法,還可以帶入值到生成器內(nèi)部 yield 的占位處,使得 Generator 內(nèi)部和外部可以通過 next 方法進(jìn)行數(shù)據(jù)通信!

好了,生成器了解的差不多了,現(xiàn)在看看把 Promise 和 Generator 放一起會產(chǎn)生什么黑魔法吧!

這里寫一個 delayGet 函數(shù)用來模擬費(fèi)時(shí)操作,延遲 1 秒返回某個值。在此借助一個 run 方法,就實(shí)現(xiàn)了同步編寫非阻塞的邏輯!這就是 TJ 大神 co 框架的基本思想。
回首一下我們曾經(jīng)的理想,那段代碼用 co 框架編寫可以是這樣的:
co(function*(){
var data1 = yield ajax('/get/data/1');
var data2 = yield ajax('/get/data/2');
var data3 = yield ajax('/get/data/3');
var result = yield dealData(data1, data2, data3);
yield sleep(1000);
var ret = yield ajax('/post/data', result.data);
//...
})();
Perfect!完美!
ES7 async-await
ES3 時(shí)代我們用閉包來模擬 private 成員,ES5 便加入了 defineProperty 。Generator 最初的本意是用來生成迭代序列的,畢竟不是為異步而生的。ES7 索性引入 async、await關(guān)鍵字。async 標(biāo)記的函數(shù)支持 await 表達(dá)式。包含 await 表達(dá)式的的函數(shù)是一個deferred function 。await 表達(dá)式的值,是一個 awaited object。當(dāng)該表達(dá)式的值被評估(evaluate) 之后,函數(shù)的執(zhí)行就被暫停(suspend)。只有當(dāng) deffered 對象執(zhí)行了回調(diào)(callback 或者 errback)后,函數(shù)才會繼續(xù)。
也就是說,只需將使用 co 框架的代碼中的 yield 換掉即可:
async function task(){
var data1 = await ajax('/get/data/1');
var data2 = await ajax('/get/data/2');
var data3 = await ajax('/get/data/3');
var result = await dealData(data1, data2, data3);
await sleep(1000);
var ret = await ajax('/post/data', result.data);
//...
}
至此,本文的全部內(nèi)容都已完畢。前端標(biāo)準(zhǔn)不斷在完善,未來會越來越美好。永遠(yuǎn)相信美好的事情即將發(fā)生!