Promise這個折磨人的小妖精

前言

不得不說堪藐, promise 這玩意届榄,是每個面試官都會問的問題浅乔,但是你真的了解promise嗎?其實(shí)我也不了解痒蓬,下面的內(nèi)容都是我從掘金童擎、知乎、《ECMAScript6入門》上看的博客文章等資料攻晒,然后總結(jié)的顾复,畢竟自己寫一遍,更有助于理解鲁捏,如有錯誤芯砸,請指出 ~

什么是回調(diào)地獄 ?

在過去寫異步代碼都要靠回調(diào)函數(shù)给梅,當(dāng)異步操作依賴于其他異步操作的返回值時假丧,會出現(xiàn)一種現(xiàn)象,被程序員稱為 “回調(diào)地獄”动羽,比如這樣 :

    // 假設(shè)我們要請求用戶數(shù)據(jù)信息包帚,它接收兩個回調(diào),假設(shè)我們要請求用戶數(shù)據(jù)信息运吓,它接收兩個回調(diào)渴邦,successCallback 和 errCallback

    function getUserInfo (successCallback, errCallback) {
        $.ajax({
            url : 'xxx',
            method : 'get',
            data : {
                user_id : '123'
            },
            success : function(res) {
                successCallback(res)    // 請求成功,執(zhí)行successCallback()回調(diào)
            },
            error : function(err) {
                errCallback(err)        // 請求失敗拘哨,執(zhí)行errCallback()回調(diào)
            }
        })
    }

騙我 谋梭? 這哪里復(fù)雜了,明明很簡單啊倦青,說好的回調(diào)地獄呢 瓮床? 不急,繼續(xù)看

假設(shè)我們拿到了用戶信息,但是我們還要拿到該用戶的聊天列表隘庄,然后再拿到跟某一“陌生”男人的聊天記錄呢 ?

    // getUserInfo -> getConnectList -> getOneManConnect()

    getUserInfo((res)=>{
        getConnectList(res.user_id, (list)=>{
            getOneManConnect(list.one_man_id, (message)=>{
                console.log('這是我和某位老男人的聊天記錄')
            }, (msg_err)=>{
                console.log('獲取詳情失敗踢步,別污蔑我,我不跟老男人聊天')
            })
        }, (list_err)=>{
            console.log('獲取列表失敗峭沦,我都不跟別人聊天')
        })
    }, (user_err)=>{
        console.log('獲取用戶個人信息失敗')
    })

大兄弟贾虽,刺激不逃糟,三層嵌套吼鱼,再多來幾個嵌套,就是 “回調(diào)地獄” 了绰咽。這時候菇肃,promise來了。

Promise 簡介

阮一峰老師的《ECMAScript 6入門》里對promise的含義是 : Promise 是異步編程的一種解決方案取募,簡單說就是一個容器琐谤,里面保存著某個未來才會結(jié)束的事件(通常是一個異步操作)的結(jié)果。從語法上說玩敏,Promise 是一個對象斗忌,從它可以獲取異步操作的消息。Promise 提供統(tǒng)一的 API旺聚,各種異步操作都可以用同樣的方法進(jìn)行處理织阳。

簡單來說,Promise就是對異步的執(zhí)行結(jié)果的描述對象砰粹。

狀態(tài)

  • pending (進(jìn)行中)
  • fulfilled (已成功)
  • rejected (已失敗)
    1 : 只有異步操作的結(jié)果唧躲,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無法改變這個狀態(tài)碱璃。
    2 : 一旦狀態(tài)改變弄痹,就不會再變,任何時候都可以得到這個結(jié)果嵌器。
    3 : Promise對象的狀態(tài)改變肛真,只有兩種可能:從pending變?yōu)閒ulfilled和從pending變?yōu)閞ejected

知乎形象例子來說明promise

// 定外賣就是一個Promise,Promise的意思就是承諾
// 我們定完外賣,飯不會立即到我們手中
// 這時候我們和商家就要達(dá)成一個承諾
// 在未來爽航,不管飯是做好了還是燒糊了蚓让,都會給我們一個答復(fù)
function 定外賣(){
    // Promise 接受兩個參數(shù)
    // resolve: 異步事件成功時調(diào)用(菜燒好了)
    // reject: 異步事件失敗時調(diào)用(菜燒糊了)
    return new Promise((resolve, reject) => {
        let result = 做飯()
    // 下面商家給出承諾,不管燒沒燒好岳掐,都會告訴你
    if (result == '菜燒好了') 
        // 商家給出了反饋
        resolve('我們的外賣正在給您派送了')
    else 
        reject('不好意思凭疮,我們菜燒糊了,您再等一會')
    })
}

// 商家廚房做飯串述,模擬概率事件
function 做飯() {
    return Math.random() > 0.5 ? '菜燒好了' : '菜燒糊了'
}

// 你在家上餓了么定外賣
// 有一半的概率會把你的飯燒糊了
// 好在有承諾执解,他還是會告訴你

定外賣()
    // 菜燒好執(zhí)行,返回'我們的外賣正在給您派送了'
    .then(res => console.log(res))
    // 菜燒糊了執(zhí)行,返回'不好意思衰腌,我們菜燒糊了新蟆,您再等一會'
    .catch(res => console.log(res))

基本用法

Promise 對象是一個構(gòu)造函數(shù),用來生成一個Promise實(shí)例右蕊。

    Promise構(gòu)造函數(shù)接受一個函數(shù)作為參數(shù)琼稻,這個函數(shù)有兩個參數(shù),分別是resolve()和reject()饶囚。

    resovle()函數(shù)是將Promise對象從pending變成fulfilled帕翻,在異步操作完成時執(zhí)行,將異步結(jié)果萝风,作為參數(shù)傳遞出去嘀掸。

    reject()函數(shù)是將Promise對象從pending變成rejected,在異步執(zhí)行失敗時執(zhí)行规惰,將報錯信息睬塌,作為參數(shù)傳遞出去。

    // 簡單的一個promise實(shí)例歇万, 來自阮一峰老師的es6 示例代碼
    const promise = new Promise((resolve, reject) => {
        // some code 

        if(/* 異步執(zhí)行成功 */) {
            resolve(res)
        } else {
            reject(error)
        }
    })

then方法

Promise 有個.then()方法揩晴,then 方法中的回調(diào)在微任務(wù)隊(duì)列中執(zhí)行,支持傳入兩個參數(shù)贪磺,一個是成功的回調(diào)硫兰,一個是失敗的回調(diào),在 Promise 中調(diào)用了 resolve 方法缘挽,就會在 then 中執(zhí)行成功的回調(diào)瞄崇,調(diào)用了 reject 方法,就會在 then 中執(zhí)行失敗的回調(diào)壕曼,成功的回調(diào)和失敗的回調(diào)只能執(zhí)行一個苏研,resolve 和 reject 方法調(diào)用時傳入的參數(shù)會傳遞給 then 方法中對應(yīng)的回調(diào)函數(shù)。

    // 執(zhí)行 resolve  
    let promise = new Promise((resolve, reject) => {
        console.log(1)
        resolve(3)
    })

    console.log(2)

    promise.then((data)=>{
        console.log(data)
    }, (err)=>{
        console.log(err)
    })

    // 1
    // 2
    // 3
    // 執(zhí)行 reject  
    let promise = new Promise((resolve, reject) => {
        console.log(1)
        reject()
    })

    promise.then(()=>{
        console.log(2)
    }, ()=>{
        console.log(3)
    })

    // 1
    // 3

then方法

[ 注意 : then方法中的回調(diào)是異步的H肌D∧ⅰ!]

為什么上面第一個示例代碼的結(jié)果是 1 -> 2 -> 3呢 轧飞?傳入Promise 中的執(zhí)行函數(shù)是立即執(zhí)行完的啊衅鹿,為什么不是立即執(zhí)行 then 中的回調(diào)呢?因?yàn)閠hen 中的回調(diào)是異步執(zhí)行过咬,表示該回調(diào)是插入事件隊(duì)列末尾大渤,在當(dāng)前的同步任務(wù)結(jié)束之后,下次事件循環(huán)開始時執(zhí)行隊(duì)列中的任務(wù)掸绞。

Promise 的回調(diào)函數(shù)不是正常的異步任務(wù)泵三,而是微任務(wù)(microtask)。它們的區(qū)別在于,正常任務(wù)追加到下一輪事件循環(huán)烫幕,微任務(wù)追加到本輪事件循環(huán)俺抽。這意味著,微任務(wù)的執(zhí)行時間一定早于正常任務(wù)

then方法的返回值是一個新的GPromise對象较曼,這就是為什么promise能夠進(jìn)行鏈?zhǔn)讲僮鞯脑颉?
then方法中的一個難點(diǎn)就是處理異步磷斧,通過setInterval來監(jiān)聽GPromise對象的狀態(tài)改變,一旦改變捷犹,就是執(zhí)行GPromise對應(yīng)的then方法中相應(yīng)的回調(diào)函數(shù)弛饭。這樣回調(diào)函數(shù)就能夠插入事件隊(duì)列末尾赎离,異步執(zhí)行研叫。
    then有兩個參數(shù) : onFulfilled 和 onRejected
    
    · 當(dāng)狀態(tài)state為fulfilled,則執(zhí)行onFulfilled,傳入this.value翠桦。當(dāng)狀態(tài)state為rejected,則執(zhí)行onRejected胳蛮,傳入this.reason

    · onFulfilled,onRejected如果他們是函數(shù)销凑,則必須分別在fulfilled,rejected后被調(diào)用仅炊,value或reason依次作為他們的第一個參數(shù)

    class Promise{
        constructor(executor){...}
        // then 方法 有兩個參數(shù)onFulfilled onRejected
        then(onFulfilled,onRejected) {
            // 狀態(tài)為fulfilled斗幼,執(zhí)行onFulfilled,傳入成功的值
            if (this.state === 'fulfilled') {
                onFulfilled(this.value);
            };
            // 狀態(tài)為rejected抚垄,執(zhí)行onRejected蜕窿,傳入失敗的原因
            if (this.state === 'rejected') {
                onRejected(this.reason);
            };
        }
    }

Promise的鏈?zhǔn)秸{(diào)用

由于promise每次調(diào)用then方法就會返回一個新的promise對象,如果該then方法中執(zhí)行的回調(diào)函數(shù)有返回值呆馁,那么這個返回值就會作為下一個promise實(shí)例的then方法回調(diào)的參數(shù)桐经,如果 then 方法的返回值是一個 Promise 實(shí)例,那就返回一個新的 Promise 實(shí)例浙滤,將 then 返回的 Promise 實(shí)例執(zhí)行后的結(jié)果作為返回 Promise 實(shí)例回調(diào)的參數(shù)阴挣。

還記得剛開頭說的那個“陌生”男人例子嗎 ?這里我們用promise的鏈?zhǔn)讲僮髦貙懴?/p>

    // 原來的代碼
    getUserInfo((res)=>{
        getConnectList(res.user_id, (list)=>{
            getOneManConnect(list.one_man_id, (message)=>{
                console.log('這是我和某位老男人的聊天記錄')
            }, (msg_err)=>{
                console.log('獲取詳情失敗纺腊,別污蔑我畔咧,我不跟老男人聊天')
            })
        }, (list_err)=>{
            console.log('獲取列表失敗,我都不跟別人聊天')
        })
    }, (user_err)=>{
        console.log('獲取用戶個人信息失敗')
    })

    
    // Promise重寫的代碼
    function handleAjax (params) {
        return new Promise((resolve, reject)=>{
            $.ajax({
                url : params.url,
                type : params.type || 'get',
                data : params.data || '',
                success : function(data) {
                    resolve(data)
                },
                error : function(error) {
                    reject(error)
                }
            })
        })
    }

    const promise = handleAjax({
        url : 'xxxx/user'
    });

    promise.then((data1)=>{
        console.log('獲取個人信息成功')       // 獲取個人信息成功
        return handleAjax({
            url : 'xxxx/user/connectlist',
            data : data1.user_id
        });
    })
    .then((data2)=>{
        console.log('獲得聊天列表')
        return handleAjax({
            url : 'xxxx/user/connectlist/one_man',
            data : data2.one_man_id
        });
    })
    .then((data3)=>{
        console.log('獲得跟某男人的聊天')
    })
    .catch((err)=>{
        console.log(err)
    }) 

來自ES6的 Promise.prototype.then()

Promise 實(shí)例具有then方法揖膜,也就是說誓沸,then方法是定義在原型對象Promise.prototype上的。它的作用是為 Promise 實(shí)例添加狀態(tài)改變時的回調(diào)函數(shù)壹粟。前面說過拜隧,then方法的第一個參數(shù)是resolved狀態(tài)的回調(diào)函數(shù),第二個參數(shù)(可選)是rejected狀態(tài)的回調(diào)函數(shù)。

then方法返回的是一個新的Promise實(shí)例(注意虹蓄,不是原來那個Promise實(shí)例)犀呼。因此可以采用鏈?zhǔn)綄懛ǎ磘hen方法后面再調(diào)用另一個then方法薇组。

采用鏈?zhǔn)降膖hen外臂,可以指定一組按照次序調(diào)用的回調(diào)函數(shù)。這時律胀,前一個回調(diào)函數(shù)宋光,有可能返回的還是一個Promise對象(即有異步操作),這時后一個回調(diào)函數(shù)炭菌,就會等待該P(yáng)romise對象的狀態(tài)發(fā)生變化罪佳,才會被調(diào)用

來自ES6的 Promise.prototype.catch()

Promise.prototype.catch方法是.then(null, rejection)的別名,用于指定發(fā)生錯誤時的回調(diào)函數(shù)黑低。Promise對象狀態(tài)變?yōu)閞esolved赘艳,則會調(diào)用then方法指定的回調(diào)函數(shù);如果異步操作拋出錯誤克握,狀態(tài)就會變?yōu)閞ejected蕾管,就會調(diào)用catch方法指定的回調(diào)函數(shù),處理這個錯誤菩暗。另外掰曾,then方法指定的回調(diào)函數(shù),如果運(yùn)行中拋出錯誤停团,也會被catch方法捕獲旷坦。

Promise 對象的錯誤具有“冒泡”性質(zhì),會一直向后傳遞佑稠,直到被捕獲為止秒梅。也就是說讶坯,錯誤總是會被下一個catch語句捕獲

一般來說,不要在then方法里面定義 reject 狀態(tài)的回調(diào)函數(shù)(即then的第二個參數(shù))辆琅,總是使用catch方法。

來自ES6的 Promise.all()

Promise.all方法用于將多個 Promise 實(shí)例婉烟,包裝成一個新的 Promise 實(shí)例娩井。

    const p = Promise.all([p1, p2, p3])

Promise.all方法接受一個數(shù)組作為參數(shù),p1洞辣、p2咐刨、p3都是 Promise 實(shí)例扬霜,如果不是定鸟,就會先調(diào)用下面講到的Promise.resolve方法,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例著瓶,再進(jìn)一步處理联予。

p的狀態(tài)由p1材原、p2、p3決定余蟹,分成兩種情況。

(1)只有p1窑睁、p2兼搏、p3的狀態(tài)都變成fulfilled卵慰,p的狀態(tài)才會變成fulfilled佛呻,此時p1病线、p2、p3的返回值組成一個數(shù)組送挑,傳遞給p的回調(diào)函數(shù)。

(2)只要p1纺裁、p2司澎、p3之中有一個被rejected,p的狀態(tài)就變成rejected挤安,此時第一個被reject的實(shí)例的返回值,會傳遞給p的回調(diào)函數(shù)嫩絮。

來自ES6 的Promise.race()

Promise.race方法同樣是將多個 Promise 實(shí)例,包裝成一個新的 Promise 實(shí)例剿干。

    const p = Promise.all([p1, p2, p3])

上面代碼中,只要p1杠步、p2撰洗、p3之中有一個實(shí)例率先改變狀態(tài),p的狀態(tài)就跟著改變差导。那個率先改變的 Promise 實(shí)例的返回值,就傳遞給p的回調(diào)函數(shù)

Promise.race方法的參數(shù)與Promise.all方法一樣设褐,如果不是 Promise 實(shí)例颠蕴,就會先調(diào)用下面講到的Promise.resolve方法助析,將參數(shù)轉(zhuǎn)為 Promise 實(shí)例,再進(jìn)一步處理寡键。

來自ES6 的Promise.resolve()

有時需要將現(xiàn)有對象轉(zhuǎn)為 Promise 對象雪隧,Promise.resolve方法就起到這個作用

    Promise.resolve('test')
    // 等價于
    new Promise(resolve => resolve('test'))

    // 更多請看阮一峰老師的ES6 Promise對象

來自ES6 的Promise.reject()

Promise.reject(reason)方法也會返回一個新的 Promise 實(shí)例,該實(shí)例的狀態(tài)為rejected藕畔。

    const p = Promise.reject('出錯了');
    // 等同于
    const p = new Promise((resolve, reject) => reject('出錯了'))

    p.then(null, function (err) {
        console.log(err)    // 出錯了
    });
    
    // 更多請看阮一峰老師的ES6 Promise對象

相關(guān)鏈接

個人博客 : https://github.com/PDKSophia/blog.io

個人掘金 : https://juejin.im/user/594ca8a35188250d892f4139

阮一峰 ES6 : http://es6.ruanyifeng.com/#docs/promise

知乎例子 : https://zhuanlan.zhihu.com/p/29632791

掘金 卡姆愛卡姆 : https://juejin.im/post/5b2f02cd5188252b937548ab

來自segmentfault 的GEEK作者 : https://segmentfault.com/a/1190000011241512

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末庄拇,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子溶弟,更是在濱河造成了極大的恐慌熄诡,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件我抠,死亡現(xiàn)場離奇詭異,居然都是意外死亡菜拓,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評論 3 395
  • 文/潘曉璐 我一進(jìn)店門俺夕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來贱鄙,“玉大人,你說我怎么就攤上這事映九∠箍牛” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評論 0 356
  • 文/不壞的土叔 我叫張陵引有,是天一觀的道長。 經(jīng)常有香客問我譬正,道長檬姥,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,842評論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮斋荞,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘凤优。我一直安慰自己蜈彼,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評論 6 392
  • 文/花漫 我一把揭開白布棍辕。 她就那樣靜靜地躺著,像睡著了一般楚昭。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上塘幅,一...
    開封第一講書人閱讀 51,679評論 1 305
  • 那天尿贫,我揣著相機(jī)與錄音,去河邊找鬼庆亡。 笑死,一個胖子當(dāng)著我的面吹牛钝尸,可吹牛的內(nèi)容都是我干的搂根。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼剩愧,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了穴翩?” 一聲冷哼從身側(cè)響起锦积,我...
    開封第一講書人閱讀 39,311評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎背蟆,沒想到半個月后哮幢,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體带膀,經(jīng)...
    沈念sama閱讀 45,767評論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡垛叨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評論 3 336
  • 正文 我和宋清朗相戀三年柜某,在試婚紗的時候發(fā)現(xiàn)自己被綠了敛纲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片还棱。...
    茶點(diǎn)故事閱讀 40,090評論 1 350
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖办铡,靈堂內(nèi)的尸體忽然破棺而出琳要,到底是詐尸還是另有隱情,我是刑警寧澤稚补,帶...
    沈念sama閱讀 35,785評論 5 346
  • 正文 年R本政府宣布课幕,位于F島的核電站厦坛,受9級特大地震影響乍惊,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜撬碟,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評論 3 331
  • 文/蒙蒙 一莉撇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧棍郎,春花似錦、人聲如沸静秆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至辐宾,卻和暖如春膨蛮,著一層夾襖步出監(jiān)牢的瞬間季研,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評論 1 271
  • 我被黑心中介騙來泰國打工惹谐, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人氨肌。 一個月前我還...
    沈念sama閱讀 48,298評論 3 372
  • 正文 我出身青樓酌畜,卻偏偏與公主長得像,于是被迫代替她去往敵國和親桥胞。 傳聞我的和親對象是個殘疾皇子恳守,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評論 2 355