回調(diào)之痛
每一位前端工程師上輩子都是折翼的天使曙聂。
相信很多前端工程師都同我一樣,初次接觸到前端時鞠鲜,了解了些許 HTML宁脊、CSS、JS 知識镊尺,便驚嘆于前端的美好朦佩,沉醉于這種所見即所得的成就感之中。但很快我就發(fā)現(xiàn)庐氮,前端并沒有想象中的那么美好语稠,JS 也并不是彈一個 alert
這么簡單。尤其是當(dāng)我想這么干弄砍,卻發(fā)現(xiàn)無法得到結(jié)果時:
var data = ajax('/url/to/data');
在查閱很多資料后仙畦,我知道了 JS 是事件驅(qū)動的,ajax 異步請求是非阻塞的音婶,我封裝的 ajax 函數(shù)無法直接返回服務(wù)器數(shù)據(jù)慨畸,除非聲明為同步請求(顯然這不是我想要的)。于是我學(xué)會了或者說接受了這樣的事實衣式,并改造了我的 ajax 函數(shù):
ajax('/url/to/data', function(data){
//deal with data
});
在很長一段時間寸士,我并沒有認(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)實太骨干。這種回調(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)時
- 可以轉(zhuǎn)移至執(zhí)行態(tài)或拒絕態(tài)
- 處于兌現(xiàn)態(tài)時
- 不能遷移至其他任何狀態(tài)
- 必須擁有一個不可變的值作為兌現(xiàn)結(jié)果
- 處于拒絕態(tài)時
- 不能遷移至其他任何狀態(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í)行時迫靖,所有onFulfilled
需按照其注冊順序依次回調(diào) - 當(dāng)
promise
被拒絕執(zhí)行時,所有的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)異步流程的實現(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 的實現(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 實現(xiàn)規(guī)范的 API 之外场靴,還實現(xiàn)了一對接口:notify/progress。這對接口在某些場合下港准,簡直太有用了旨剥,例如倒計時功能。對上述 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 帶來的美好。比如衩椒,實現(xiàn)一個 60 秒后重新獲取驗證碼的功能:
var btn = $("#getSMSCodeBtn");
btn.addClass("disabled");
countDown(60)
.progress(function(s){
btn.val(s+'秒后可重新獲取');
})
.then(function(){
btn.val('重新獲取驗證碼').removeClass('disabled');
});
簡直驚艷蚌父!離絕對的同步編寫非阻塞形式的代碼已經(jīng)很近了!
與 ES6 Generator 碰撞出火花
我深刻感受到毛萌,前端技術(shù)發(fā)展是這樣一種狀況: 當(dāng)我們驚嘆于最新技術(shù)標(biāo)準(zhǔn)的美好苟弛,感覺一個最好的時代即將到來時,回到實際生產(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)在還不能將之投入使用(如果你需要婿斥,其實可以通過 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)用時,將觸發(fā)生成器中的代碼執(zhí)行娃胆,執(zhí)行到 yield
語句時遍希,會將 yield
后的值帶出到迭代器的 next
方法的返回值中,并保存好運(yùn)行時環(huán)境里烦,將代碼掛起凿蒜,直到下一次 next
方法被調(diào)用時繼續(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)時操作,延遲 1 秒返回某個值披蕉。在此借助一個 run 方法颈畸,就實現(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 時代我們用閉包來模擬 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ā)生!