承諾之美 —— 淺談基于 Promise 的異步 Javascript編程方法

回調(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)中的一種之中碧绞。

  1. 處于等待態(tài)時
  • 可以轉(zhuǎn)移至執(zhí)行態(tài)或拒絕態(tài)
  1. 處于兌現(xiàn)態(tài)時
  • 不能遷移至其他任何狀態(tài)
  • 必須擁有一個不可變的值作為兌現(xiàn)結(jié)果
  1. 處于拒絕態(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 方法不斷迭代蛔钙,直到迭代完畢。

生成器-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í)行废封!天然的異步有木有!感受一下這個例子:

生成器-dead-loop

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

生成器-interact

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

生成器-Promise

這里寫一個 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ā)生!

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椒丧,一起剝皮案震驚了整個濱河市壹甥,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌壶熏,老刑警劉巖句柠,帶你破解...
    沈念sama閱讀 206,214評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異棒假,居然都是意外死亡溯职,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評論 2 382
  • 文/潘曉璐 我一進(jìn)店門帽哑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來谜酒,“玉大人,你說我怎么就攤上這事妻枕∑ё澹” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評論 0 341
  • 文/不壞的土叔 我叫張陵屡谐,是天一觀的道長述么。 經(jīng)常有香客問我,道長愕掏,這世上最難降的妖魔是什么度秘? 我笑而不...
    開封第一講書人閱讀 55,221評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮亭珍,結(jié)果婚禮上敷钾,老公的妹妹穿的比我還像新娘枝哄。我一直安慰自己,他們只是感情好阻荒,可當(dāng)我...
    茶點故事閱讀 64,224評論 5 371
  • 文/花漫 我一把揭開白布挠锥。 她就那樣靜靜地躺著,像睡著了一般侨赡。 火紅的嫁衣襯著肌膚如雪蓖租。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評論 1 284
  • 那天羊壹,我揣著相機(jī)與錄音蓖宦,去河邊找鬼。 笑死油猫,一個胖子當(dāng)著我的面吹牛稠茂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播情妖,決...
    沈念sama閱讀 38,313評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼睬关,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了毡证?” 一聲冷哼從身側(cè)響起电爹,我...
    開封第一講書人閱讀 36,956評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎料睛,沒想到半個月后丐箩,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡恤煞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,925評論 2 323
  • 正文 我和宋清朗相戀三年屎勘,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片阱州。...
    茶點故事閱讀 38,018評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡挑秉,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出苔货,到底是詐尸還是另有隱情犀概,我是刑警寧澤,帶...
    沈念sama閱讀 33,685評論 4 322
  • 正文 年R本政府宣布夜惭,位于F島的核電站姻灶,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏诈茧。R本人自食惡果不足惜产喉,卻給世界環(huán)境...
    茶點故事閱讀 39,234評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧曾沈,春花似錦这嚣、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至障涯,卻和暖如春罐旗,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背唯蝶。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評論 1 261
  • 我被黑心中介騙來泰國打工九秀, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人粘我。 一個月前我還...
    沈念sama閱讀 45,467評論 2 352
  • 正文 我出身青樓鼓蜒,卻偏偏與公主長得像,于是被迫代替她去往敵國和親征字。 傳聞我的和親對象是個殘疾皇子友酱,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 42,762評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 異步編程對JavaScript語言太重要。Javascript語言的執(zhí)行環(huán)境是“單線程”的柔纵,如果沒有異步編程,根本...
    呼呼哥閱讀 7,298評論 5 22
  • 官方中文版原文鏈接 感謝社區(qū)中各位的大力支持锤躁,譯者再次奉上一點點福利:阿里云產(chǎn)品券搁料,享受所有官網(wǎng)優(yōu)惠,并抽取幸運(yùn)大...
    HetfieldJoe閱讀 6,372評論 9 19
  • 簡介 基本概念 Generator函數(shù)是ES6提供的一種異步編程解決方案系羞,語法行為與傳統(tǒng)函數(shù)完全不同郭计。本章詳細(xì)介紹...
    呼呼哥閱讀 1,068評論 0 4
  • 在此處先列下本篇文章的主要內(nèi)容 簡介 next方法的參數(shù) for...of循環(huán) Generator.prototy...
    醉生夢死閱讀 1,436評論 3 8
  • 弄懂js異步 講異步之前,我們必須掌握一個基礎(chǔ)知識-event-loop椒振。 我們知道JavaScript的一大特點...
    DCbryant閱讀 2,695評論 0 5