使用Promise解決多層異步調(diào)用的簡單學習

前言

第一次接觸到Promise這個東西应役,是2012年微軟發(fā)布Windows8操作系統(tǒng)后抱著作死好奇的心態(tài)研究用html5寫Metro應(yīng)用的時候库倘。當時配合html5提供的WinJS庫里面的異步接口全都是Promise形式,這對那時候剛剛畢業(yè)一點javascript基礎(chǔ)都沒有的我而言簡直就是天書。我當時想的是,微軟又在腦洞大開的瞎搗鼓了各谚。

結(jié)果沒想到,到了2015年到千,Promise居然寫進ES6標準里面了昌渤。而且一項調(diào)查顯示,js程序員們用這玩意用的還挺high父阻。

諷刺的是愈涩,作為早在2012年就在Metro應(yīng)用開發(fā)接口里面廣泛使用Promise的微軟,其自家瀏覽器IE直到2015年壽終正寢了都還不支持Promise加矛,看來微軟不是沒有這個技術(shù)履婉,而是真的對IE放棄治療了。斟览。毁腿。

現(xiàn)在回想起來,當時看到Promise最頭疼的,就是初學者看起來匪夷所思已烤,也是最被js程序員廣為稱道的特性:then函數(shù)調(diào)用鏈鸠窗。

then函數(shù)調(diào)用鏈,從其本質(zhì)上而言胯究,就是對多個異步過程的依次調(diào)用稍计,本文就從這一點著手,對Promise這一特性進行研究和學習裕循。

Promise解決的問題

考慮如下場景臣嚣,函數(shù)延時2秒之后打印一行日志,再延時3秒打印一行日志剥哑,再延時4秒打印一行日志硅则,這在其他的編程語言當中是非常簡單的事情,但是到了js里面就比較費勁株婴,代碼大約會寫成下面的樣子:

var myfunc = function() {   
    setTimeout(function() {
        console.log("log1");
        setTimeout(function() {
            console.log("log2");
            setTimeout(function() {
                console.log("log3");
            }, 4000);
        }, 3000); 
    }, 2000);
}

由于嵌套了多層回調(diào)結(jié)構(gòu)怎虫,這里形成了一個典型的金字塔結(jié)構(gòu)。如果業(yè)務(wù)邏輯再復雜一些困介,就會變成令人聞風喪膽的回調(diào)地獄大审。

如果意識比較好,知道提煉出簡單的函數(shù)座哩,那么代碼差不多是這個樣子:

var func1 = function() {
    setTimeout(func2, 2000);
};

var func2 = function() {
    console.log("log1");
    setTimeout(func3, 3000);
};

var func3 = function() {
    console.log("log2");
    setTimeout(func4, 4000);
};

var func4 = function() {
    console.log("log3");
};

這樣看起來稍微好一點了饥努,但是總覺得有點怪怪的。八回。。好吧驾诈,其實我js水平有限缠诅,說不上來為什么這樣寫不好。如果你知道為什么這樣寫不太好所以發(fā)明了Promise乍迄,請告訴我管引。

現(xiàn)在讓我們言歸正傳,說說Promise這個東西闯两。

Promise的描述

這里請允許我引用MDN對Promise的描述:

Promise 對象用于延遲(deferred) 計算和異步(asynchronous ) 計算.褥伴。一個Promise對象代表著一個還未完成,但預(yù)期將來會完成的操作漾狼。

Promise 對象是一個返回值的代理重慢,這個返回值在promise對象創(chuàng)建時未必已知。它允許你為異步操作的成功或失敗指定處理方法逊躁。 這使得異步方法可以像同步方法那樣返回值:異步方法會返回一個包含了原返回值的 promise 對象來替代原返回值似踱。

Promise對象有以下幾種狀態(tài):

  • pending: 初始狀態(tài), 非 fulfilled 或 rejected。
  • fulfilled: 成功的操作。
  • rejected: 失敗的操作核芽。

pending狀態(tài)的promise對象既可轉(zhuǎn)換為帶著一個成功值的fulfilled 狀態(tài)囚戚,也可變?yōu)閹е粋€失敗信息的 rejected 狀態(tài)。當狀態(tài)發(fā)生轉(zhuǎn)換時轧简,promise.then綁定的方法(函數(shù)句柄)就會被調(diào)用驰坊。(當綁定方法時,如果 promise對象已經(jīng)處于 fulfilled 或 rejected 狀態(tài)哮独,那么相應(yīng)的方法將會被立刻調(diào)用拳芙, 所以在異步操作的完成情況和它的綁定方法之間不存在競爭條件。)

更多關(guān)于Promise的描述和示例可以參考MDN的Promise條目借嗽,或者MSDN的Promise條目态鳖。

嘗試使用Promise解決我們的問題

基于以上對Promise的了解,我們知道可以使用它來解決多層回調(diào)嵌套后的代碼蠢笨難以維護的問題恶导。關(guān)于Promise的語法和參數(shù)上面給出的兩個鏈接已經(jīng)說的很清楚了浆竭,這里不重復,直接上代碼惨寿。

我們先來嘗試一個比較簡單的情況邦泄,只執(zhí)行一次延時和回調(diào):

new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout");
    setTimeout(res, 2000);
}).then(function() {
    console.log(Date.now() + " timeout call back");
});

看起來和MSDN里的示例也沒什么區(qū)別,執(zhí)行結(jié)果如下:

$ node promisTest.js
1450194136374 start setTimeout
1450194138391 timeout call back

那么如果我們要再做一個延時呢裂垦,那么我可以這樣寫:

new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 1");
    setTimeout(res, 2000);
}).then(function() {
    console.log(Date.now() + " timeout 1 call back");
    new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
    }).then(function() {
        console.log(Date.now() + " timeout 2 call back");
    })
});

似乎也能正確運行:

$ node promisTest.js
1450194338710 start setTimeout 1
1450194340720 timeout 1 call back
1450194340720 start setTimeout 2
1450194343722 timeout 2 call back

不過代碼看起來蠢萌蠢萌的是不是顺囊,而且隱約又在搭金字塔了。這和引入Promise的目的背道而馳蕉拢。

那么問題出在哪呢特碳?正確的姿勢又是怎樣的?

答案藏在then函數(shù)以及then函數(shù)的onFulfilled(或者叫onCompleted)回調(diào)函數(shù)的返回值里面晕换。

首先明確的一點是午乓,then函數(shù)會返回一個新的Promise變量,你可以再次調(diào)用這個新的Promise變量的then函數(shù)闸准,像這樣:

new Promise(...).then(...)
    .then(...).then(...).then(...)...

then函數(shù)返回的是什么樣的Promies益愈,取決于onFulfilled回調(diào)的返回值。

事實上夷家,onFulfilled可以返回一個普通的變量蒸其,也可以是另一個Promise變量。

如果onFulfilled返回的是一個普通的值库快,那么then函數(shù)會返回一個默認的Promise變量摸袁。執(zhí)行這個Promise的then函數(shù)會使Promise立即被滿足,執(zhí)行onFulfilled函數(shù)缺谴,而這個onFulfilled的入?yún)⒌蹋词巧弦粋€onFulfilled的返回值耳鸯。

而如果onFulfilled返回的是一個Promise變量,那個這個Promise變量就會作為then函數(shù)的返回值膀曾。

關(guān)于then函數(shù)和onFulfilled函數(shù)的返回值的這一系列設(shè)定县爬,MDN和MSDN上的文檔都沒有明確的正面描述,至于ES6官方文檔ECMAScript 2015 (6th Edition, ECMA-262)添谊。财喳。。我的水平有限實在看不懂斩狱,如果哪位高手能解釋清楚官方文檔里面對著兩個返回值的描述耳高,請一定留言指教!K弧泌枪!

所以以上為我的自由發(fā)揮,語言組織的有點拗口秕岛,上代碼看一下大家就明白了碌燕。

首先是返回普通變量的情況:

new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 1");
    setTimeout(res, 2000);
}).then(function() {
    console.log(Date.now() + " timeout 1 call back");
    return 1024;
}).then(function(arg) {
    console.log(Date.now() + " last onFulfilled return " + arg);    
});

以上代碼執(zhí)行結(jié)果為:

$ node promisTest.js
1450277122125 start setTimeout 1
1450277124129 timeout 1 call back
1450277124129 last onFulfilled return 1024

有點意思對不對,但這不是關(guān)鍵继薛。關(guān)鍵是onFulfilled函數(shù)返回一個Promise變量可以使我們很方便的連續(xù)調(diào)用多個異步過程修壕。比如我們可以這樣來嘗試連續(xù)做兩個延時操作:

new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 1");
    setTimeout(res, 2000);
}).then(function() {
    console.log(Date.now() + " timeout 1 call back");
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
    });
}).then(function() {
    console.log(Date.now() + " timeout 2 call back");
});

執(zhí)行結(jié)果如下:

$ node promisTest.js
1450277510275 start setTimeout 1
1450277512276 timeout 1 call back
1450277512276 start setTimeout 2
1450277515327 timeout 2 call back

如果覺得這也沒什么了不起,那再多來幾次也不在話下:

new Promise(function(res, rej) {
    console.log(Date.now() + " start setTimeout 1");
    setTimeout(res, 2000);
}).then(function() {
    console.log(Date.now() + " timeout 1 call back");
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 2");
        setTimeout(res, 3000);
    });
}).then(function() {
    console.log(Date.now() + " timeout 2 call back");
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 3");
        setTimeout(res, 4000);
    });
}).then(function() {
    console.log(Date.now() + " timeout 3 call back");
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start setTimeout 4");
        setTimeout(res, 5000);
    });
}).then(function() {
    console.log(Date.now() + " timeout 4 call back");
});
$ node promisTest.js
1450277902714 start setTimeout 1
1450277904722 timeout 1 call back
1450277904724 start setTimeout 2
1450277907725 timeout 2 call back
1450277907725 start setTimeout 3
1450277911730 timeout 3 call back
1450277911730 start setTimeout 4
1450277916744 timeout 4 call back

可以看到遏考,多個延時的回調(diào)函數(shù)被有序的排列下來慈鸠,并沒有出現(xiàn)喜聞樂見的金字塔狀結(jié)構(gòu)。雖然代碼里面調(diào)用的都是異步過程灌具,但是看起來就像是全部由同步過程構(gòu)成的一樣青团。這就是Promise帶給我們的好處。

如果你有把啰嗦的代碼提煉成單獨函數(shù)的好習慣咖楣,那就更加畫美不看了:

function timeout1() {
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout1");
        setTimeout(res, 2000);
    });
}

function timeout2() {
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout2");
        setTimeout(res, 3000);
    });
}

function timeout3() {
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout3");
        setTimeout(res, 4000);
    });
}

function timeout4() {
    return new Promise(function(res, rej) {
        console.log(Date.now() + " start timeout4");
        setTimeout(res, 5000);
    });
}

timeout1()
    .then(timeout2)
    .then(timeout3)
    .then(timeout4)
    .then(function() {
        console.log(Date.now() + " timout4 callback");
    });
$ node promisTest.js
1450278983342 start timeout1
1450278985343 start timeout2
1450278988351 start timeout3
1450278992356 start timeout4
1450278997370 timout4 callback

接下來我們可以再繼續(xù)研究一下onFulfilled函數(shù)傳入入?yún)⒌膯栴}壶冒。

我們已經(jīng)知道,如果上一個onFulfilled函數(shù)返回了一個普通的值截歉,那么這個值為作為這個onFulfilled函數(shù)的入?yún)ⅲ荒敲慈绻弦粋€onFulfilled返回了一個Promise變量烟零,這個onFulfilled的入?yún)⒂謥碜阅睦铮?/p>

答案是瘪松,這個onFulfilled函數(shù)的入?yún)ⅲ巧弦粋€Promise中調(diào)用resolve函數(shù)時傳入的值锨阿。

跳躍的有點大一時間無法接受對不對宵睦,讓我們來好好縷一縷。

首先墅诡,Promise.resolve這個函數(shù)是什么壳嚎,用MDN上面文鄒鄒的說法

用成功值value解決一個Promise對象。如果該value為可繼續(xù)的(thenable,即帶有then方法)烟馅,返回的Promise對象會“跟隨”這個value说庭,采用這個value的最終狀態(tài);否則的話返回值會用這個value滿足(fullfil)返回的Promise對象郑趁。

簡而言之刊驴,這就是異步調(diào)用成功情況下的回調(diào)。

我們來看看普通的異步接口中寡润,成功情況的回調(diào)是什么樣的捆憎,就拿nodejs的上的fs.readFile(file[, options], callback)來說,它的典型調(diào)用例子如下

fs.readFile('/etc/passwd', function (err, data) {
  if (err) throw err;
  console.log(data);
});

因為對于fs.readFile這個函數(shù)而言梭纹,無論成功還是失敗躲惰,它都會調(diào)用callback這個回調(diào)函數(shù),所以這個回調(diào)接受兩個入?yún)⒈涑椋词r的異常描述err和成功時的返回結(jié)果data础拨。

那么假如我們用Promise來重構(gòu)這個讀取文件的例子,我們應(yīng)該怎么寫呢瞬沦?

首先是封裝fs.readFile函數(shù):

function readFile(fileName) {
    return new Promise(function(resolve, reject) {
        fs.readFile(fileName, function (err, data) {
            if (err) {
                reject(err);
            } else {
                resolve(data);
            }
        });
    });
}

其次是調(diào)用:

readFile('theFile.txt').then(
    function(data) {
        console.log(data);
    }, 
    function(err) {
        throw err;
    }   
);

想象一下太伊,在其他語言的讀取文件的同步調(diào)用接口的里面,文件的內(nèi)容通常是放在哪里逛钻?函數(shù)返回值對不對僚焦!答案出來了,這個resolve的入?yún)⑹鞘裁词锒唬烤褪钱惒秸{(diào)用成功情況下的返回值芳悲。

有了這個概念之后,我們就不難理解“onFulfilled函數(shù)的入?yún)⒈呃ぃ巧弦粋€Promise中調(diào)用resolve函數(shù)時傳入的值”這件事了名扛。因為onFulfilled的任務(wù),就是對上一個異步調(diào)用成功后的結(jié)果做處理的茧痒。

哎終于理順了肮韧。。旺订。

總結(jié)

下面請允許我用一段代碼對本文講解到的要點進行總結(jié):

function callp1() {
    console.log(Date.now() + " start callp1");
    return new Promise(function(res, rej) {
        setTimeout(res, 2000);
    });
}

function callp2() {
    console.log(Date.now() + " start callp2");
    return new Promise(function(res, rej) {
        setTimeout(function() {
            res({arg1: 4, arg2: "arg2 value"});
        }, 3000);
    });
}

function callp3(arg) {
    console.log(Date.now() + " start callp3 with arg = " + arg);
    return new Promise(function(res, rej) {
        setTimeout(function() {
            res("callp3");
        }, arg * 1000);
    });
}

callp1().then(function() {
    console.log(Date.now() + " callp1 return");
    return callp2();
}).then(function(ret) {
    console.log(Date.now() + " callp2 return with ret value = " + JSON.stringify(ret));
    return callp3(ret.arg1);
}).then(function(ret) {
    console.log(Date.now() + " callp3 return with ret value = " + ret);
})
$ node promisTest.js
1450191479575 start callp1
1450191481597 callp1 return
1450191481599 start callp2
1450191484605 callp2 return with ret value = {"arg1":4,"arg2":"arg2 value"}
1450191484605 start callp3 with arg = 4
1450191488610 callp3 return with ret value = callp3

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末弄企,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子区拳,更是在濱河造成了極大的恐慌拘领,老刑警劉巖,帶你破解...
    沈念sama閱讀 216,591評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件樱调,死亡現(xiàn)場離奇詭異约素,居然都是意外死亡届良,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,448評論 3 392
  • 文/潘曉璐 我一進店門圣猎,熙熙樓的掌柜王于貴愁眉苦臉地迎上來士葫,“玉大人,你說我怎么就攤上這事样漆∥希” “怎么了?”我有些...
    開封第一講書人閱讀 162,823評論 0 353
  • 文/不壞的土叔 我叫張陵放祟,是天一觀的道長鳍怨。 經(jīng)常有香客問我,道長跪妥,這世上最難降的妖魔是什么鞋喇? 我笑而不...
    開封第一講書人閱讀 58,204評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮眉撵,結(jié)果婚禮上侦香,老公的妹妹穿的比我還像新娘。我一直安慰自己纽疟,他們只是感情好罐韩,可當我...
    茶點故事閱讀 67,228評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著污朽,像睡著了一般散吵。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上蟆肆,一...
    開封第一講書人閱讀 51,190評論 1 299
  • 那天矾睦,我揣著相機與錄音,去河邊找鬼炎功。 笑死枚冗,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的蛇损。 我是一名探鬼主播赁温,決...
    沈念sama閱讀 40,078評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼淤齐!你這毒婦竟也來了束世?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,923評論 0 274
  • 序言:老撾萬榮一對情侶失蹤床玻,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后沉帮,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體锈死,經(jīng)...
    沈念sama閱讀 45,334評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡贫堰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,550評論 2 333
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了待牵。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片其屏。...
    茶點故事閱讀 39,727評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缨该,靈堂內(nèi)的尸體忽然破棺而出偎行,到底是詐尸還是另有隱情,我是刑警寧澤贰拿,帶...
    沈念sama閱讀 35,428評論 5 343
  • 正文 年R本政府宣布蛤袒,位于F島的核電站,受9級特大地震影響膨更,放射性物質(zhì)發(fā)生泄漏妙真。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,022評論 3 326
  • 文/蒙蒙 一荚守、第九天 我趴在偏房一處隱蔽的房頂上張望珍德。 院中可真熱鬧,春花似錦矗漾、人聲如沸锈候。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,672評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽泵琳。三九已至,卻和暖如春嫡锌,著一層夾襖步出監(jiān)牢的瞬間虑稼,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,826評論 1 269
  • 我被黑心中介騙來泰國打工势木, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留蛛倦,地道東北人。 一個月前我還...
    沈念sama閱讀 47,734評論 2 368
  • 正文 我出身青樓啦桌,卻偏偏與公主長得像溯壶,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子甫男,可洞房花燭夜當晚...
    茶點故事閱讀 44,619評論 2 354

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