Promise完全詳解

這篇文章是我讀《你不知道的js》時(shí)做的筆記,如有錯(cuò)誤和疑惑請(qǐng)?jiān)谠u(píng)論區(qū)指出,查看代碼高亮優(yōu)化版原文請(qǐng)點(diǎn)擊鏈接,歡迎watch和star

類比

比如我們?cè)诟叻迤谌湲?dāng)勞點(diǎn)餐,告訴服務(wù)員要一個(gè)漢堡,服務(wù)員給你一個(gè)訂單收據(jù)上面印著2204,告訴你先等著取餐。這里訂單號(hào)即為一個(gè)Promise,保證最后我會(huì)得到我的漢堡。這時(shí)你要拿好你的訂單收據(jù),用來(lái)拿他和漢堡交換;而且在排隊(duì)等餐的過(guò)程中,你可能會(huì)刷刷手機(jī)或是和朋友說(shuō)要一起吃漢堡;你拿到訂單號(hào)時(shí)也不會(huì)去想這個(gè)漢堡,盡管你很想吃,這是因?yàn)槟阋呀?jīng)把訂單號(hào)當(dāng)作了漢堡的占位符;從根本上來(lái)說(shuō),這個(gè)占位符,使這個(gè)值不再依賴時(shí)間,這是一個(gè)未來(lái)值。最終屏幕上印著取餐單號(hào)2204,你拿著訂單收據(jù),把他交給收銀員,最后換來(lái)了漢堡。也就是說(shuō),一旦你需要的值(漢堡)準(zhǔn)備好了,你就需要用承諾值(訂單收據(jù))來(lái)?yè)Q取這個(gè)值本身。

但也有一種情況是,你去收銀臺(tái)拿漢堡時(shí),服務(wù)員說(shuō),你要的漢堡賣完了。這時(shí)未來(lái)值還有一個(gè)重要特性:他可能成功、也會(huì)失敗。每次點(diǎn)漢堡時(shí),要么會(huì)得到一個(gè)漢堡,要么會(huì)得到一個(gè)售罄消息。

現(xiàn)在值與未來(lái)值

var x,y=2
x+y // NaN  x沒(méi)有被決議(resolved)

+運(yùn)算符不會(huì)等待x,y都準(zhǔn)備好,再進(jìn)行運(yùn)算。如果有一種方式,來(lái)判斷兩個(gè)值的準(zhǔn)備狀態(tài),如果任何一個(gè)沒(méi)有準(zhǔn)備好,就等待二者都準(zhǔn)備好,在進(jìn)行后面的計(jì)算。promise為了統(tǒng)一處理現(xiàn)在和將來(lái),所有的操作都成了異步。

Promise值

用promise改寫上述代碼,使兩個(gè)值都準(zhǔn)備好后,再進(jìn)行加法操作。

function add(x,y){
    return Promise.all([x,y])
    .then(values=>values[0]+values[1])
}

// fetchX(),fetchY()返回相應(yīng)的promise,就緒狀態(tài)未知
add(fetchX(),fetchY())
.then(sum=>console.log(sum))

fetchX和fetchY先直接調(diào)用,返回一個(gè)promise,傳給add。add創(chuàng)建并返回一個(gè)Promise,通過(guò)調(diào)用then等待promise,add加運(yùn)算完成后,sum已經(jīng)準(zhǔn)備好了(resolve),將會(huì)打印出來(lái)。

漢堡有可能售罄,程序也可能出錯(cuò),這時(shí),promise的決議狀態(tài)為拒絕而不是完成(可能是程序邏輯直接設(shè)置的,也有可能是runtime異常隱式得出的值)。

從外部看,由于promise封裝了依賴于時(shí)間的狀態(tài)(等待底層值的完成或拒絕,promise本身是與時(shí)間無(wú)關(guān)的),他可以按照可預(yù)測(cè)的方式組成,不需要開(kāi)發(fā)者關(guān)心時(shí)序或底層的結(jié)果。一旦promise決議,此刻他就成為了外部不可變的值。

完成事件

promise可以說(shuō)是一種在異步任務(wù)中的流程控制機(jī)制。我們無(wú)需知道他要在什么時(shí)候開(kāi)始,什么時(shí)候結(jié)束;我們只需要在他完成后發(fā)起一個(gè)通知,得到這個(gè)通知后我們來(lái)進(jìn)行下一個(gè)任務(wù)。如下代碼,foo執(zhí)行完成后,建立一個(gè)listener時(shí)間通知處理對(duì)象;然后建立兩個(gè)事件監(jiān)聽(tīng)器,一個(gè)監(jiān)聽(tīng)"completion",一個(gè)監(jiān)聽(tīng)"failure"。

function foo(){
    // 。。。耗時(shí)的工作
    return listener
}
const evt=foo(20)
evt.on('completion',()=>{
    // 進(jìn)行下一步
})
evt.on('failure',err=>{
    // foo中出錯(cuò)了
})

我們可以把這個(gè)事件監(jiān)聽(tīng)對(duì)象(evt)提供給代碼中多個(gè)獨(dú)立的部分,他們可以獨(dú)立的得到通知,執(zhí)行下一步

const evt=foo(20)
bar(evt) // bar()來(lái)監(jiān)聽(tīng)foo的完成
baz(evt) // baz()也可以來(lái)監(jiān)聽(tīng)foo的完成

foo()無(wú)需知道bar()和baz()是否存在,evt對(duì)象就是分離的關(guān)注點(diǎn)之間的中立的第三方協(xié)商機(jī)制,也是promise的一個(gè)模擬。

function foo(){
    // 。。。耗時(shí)的工作
    // 構(gòu)造并返回promise
    return new Promise((resolve,reject)=>{
        // 這里的函數(shù)會(huì)立即執(zhí)行
    })
}
const p=foo()
bar(p)
baz(p)

這里p并不是被傳給了bar()和baz(),而是使用p來(lái)控制這兩個(gè)函數(shù)何時(shí)執(zhí)行。

then方法

注意,所有的委托或值中,都不能存在自定義的then方法,否則這個(gè)值在promise系統(tǒng)中會(huì)被誤認(rèn)為是一個(gè)thenable,會(huì)造成難以追蹤的bug!!

promise值得信任嗎

以下給出了,在開(kāi)發(fā)時(shí)我們會(huì)遇到的問(wèn)題,以及promise的對(duì)應(yīng)處理方式

調(diào)用過(guò)早

當(dāng)promise已經(jīng)決議后,提供給then()的回調(diào)總會(huì)被異步調(diào)用,不需要自己插入setTimeout()

調(diào)用過(guò)晚

一旦promise決議后,這個(gè)promise上所有通過(guò)then()注冊(cè)的回調(diào)都會(huì)在下一個(gè)異步時(shí)間點(diǎn)上依次被調(diào)用。這些回調(diào)中的任何一個(gè)都不會(huì)影響其他回調(diào)的調(diào)用,如下:

p.then(()=>{
    p.then(()=>{
        console.log('C') //c無(wú)法打斷b
    })
    console.log('A')
})
p.then(()=>{console.log('B')})
// A B C

回調(diào)未調(diào)用

沒(méi)有任何東西可以阻止promise通知他的決議;如果你對(duì)一個(gè)promise注冊(cè)了完成回調(diào)和拒絕回調(diào),那么在promise決議時(shí)總是會(huì)調(diào)用其中一個(gè)。

那么,如果promise本身永遠(yuǎn)不會(huì)決議呢?我們可以創(chuàng)建一個(gè)用于超時(shí)的promise工具,設(shè)置超時(shí)競(jìng)態(tài)回調(diào),這將在后面討論promise api時(shí)給出答案。

調(diào)用次數(shù)過(guò)少或過(guò)多

正常的調(diào)用次數(shù)為1,過(guò)少為0,即為未被調(diào)用;調(diào)用過(guò)多,如代碼中出現(xiàn)多個(gè)resolve()reject(),那么這個(gè)promise將會(huì)只接受第一次決議,并忽略后續(xù)任何調(diào)用??!

未能傳遞參數(shù)/環(huán)境值

如果你沒(méi)有用任何值在promise中顯式?jīng)Q議(即沒(méi)有調(diào)用resolve或reject),這個(gè)值為undefined;它會(huì)被傳給所有的注冊(cè)回調(diào)。

或者你要傳遞多個(gè)值,那么就把它們放在數(shù)組中進(jìn)行處理。

吞掉錯(cuò)誤或異常

如果在promise創(chuàng)建中,出現(xiàn)了一個(gè)javascript一場(chǎng)錯(cuò)誤,這個(gè)異常會(huì)被捕捉,并且使這個(gè)promise被拒絕。如下:

const p=new Promise((resove,reject)=>{
    foo.bar() // foo沒(méi)有被定義,這里拋出錯(cuò)誤
    resolve(20)
})
p.then((num)=>{
    // 不會(huì)被輸出
    console.log(num)
},(err)=>{
    // err會(huì)是一個(gè)typeerror異常對(duì)象
})

傳入的值是否為一個(gè)可信的promise

在上一節(jié)提到,不要傳入含有then()的值!那么,在我們無(wú)法確定時(shí)該怎樣處理呢?

先舉個(gè)反例:

const p={
    then(cb,errcb){
        cb(20)
        errcb('this is err')
    }
}

p.then(
(val)=>{console.log(val)}, // 20
// 這里不應(yīng)該被運(yùn)行?。?(err)=>{console.log(err)} // this is err
)

就像一個(gè)普通的函數(shù)一樣運(yùn)行了,并不是promise的運(yùn)行機(jī)制。但我們可以用Promise.resove()封裝下,就會(huì)得到期望的結(jié)果。

Promise.resolve(p)
    .then(
        (val)=>{console.log(val)},
        // 永遠(yuǎn)不會(huì)到達(dá)這里!
        (err)=>{console.log(err)} 
        )

鏈?zhǔn)搅?/h3>

我們可以把多個(gè)promise連接到一起表示一系列異步步驟,這是基于promise的兩個(gè)固有行為特性:

每次對(duì)promise調(diào)用then,都會(huì)創(chuàng)建并返回一個(gè)新的promise,我們可以將其鏈接起來(lái)
不管從then()調(diào)用的完成回調(diào)(第一個(gè)參數(shù))返回的值是什么,都會(huì)被自動(dòng)設(shè)置為被鏈接的promise的完成。

如下,我們很容易把promise連接到一起:

const p=Promise.resolve(10)
    p.then((val)=>{return val*2})
    p.then((val)=>{console.log(val)}) // 20

并且,不論我們想要多少個(gè)異步步驟,每一步都能根據(jù)需要等待下一步;當(dāng)然,如果不顯式返回一個(gè)值,就會(huì)隱式返回undefined。

const p=Promise.resolve(10)
    p.then((val)=>{
        return new Promise((resolve,reject)=>{
            setTimeout(()=>{
                resolve(val*2)
            },500)
        })
    }).then((val)=>{console.log(val)}) // 20

在鏈?zhǔn)秸{(diào)用中,如果在某個(gè)步驟上出現(xiàn)了錯(cuò)誤,則會(huì)在最近的reject上處理,如果沒(méi)有,則會(huì)拋出錯(cuò)誤。

const p=Promise.resolve(10)
    p.then((val)=>{ //第一個(gè)then
        return new Promise((resolve,reject)=>{
            resolve(val*2)
            foo.bar() // 這里不會(huì)執(zhí)行,因此并不拋出錯(cuò)誤,最后打印60
        })
    })
    .then((val)=>{ // 第二個(gè)then
        return val*3
    },
    ()=>{
        console.log('oops!!')
    })
    .then(val=>{ // 第三個(gè)then
        console.log(val)
    },
    ()=>{
        console.log('oops!!2')
    })

如果將resolve(val*2)foo.bar()換個(gè)位置,foo.bar沒(méi)有聲明,此時(shí)promise決議為拒絕狀態(tài),因?yàn)闆](méi)有設(shè)置reject值,所以向下傳遞undefined值;在向下遇到的首個(gè)reject(第二個(gè)then中的reject)被捕捉,輸出oops!;由于這個(gè)then節(jié)點(diǎn)沒(méi)有出現(xiàn)錯(cuò)誤,也沒(méi)有返回值,則向下傳遞,直到被第三個(gè)then中的resolve捕捉,輸出undefined。

如果將foo.bar()放到第二個(gè)then中,則會(huì)打印'oops!!2';將foo.bar()放到第三個(gè)then中,其后面的鏈沒(méi)有reject,則會(huì)拋出錯(cuò)誤。

錯(cuò)誤處理

我們比較熟悉的try..catch只能是同步的,無(wú)法用于異步代碼模式。

但從上一節(jié)我們也知道promise是可以捕捉到異步錯(cuò)誤,但必須是在出錯(cuò)的地方的下一鏈中有reject才會(huì)進(jìn)行處理,否則只能拋出錯(cuò)誤。對(duì)此,許多開(kāi)發(fā)者常使用如下的實(shí)踐,來(lái)解決上述問(wèn)題:

const handleErrors=(err)=>{console.log(err)}
const p=Promise.resolve(10)
p.then((val)=>{
    foo.bar()
    return val*2
})
.catch(handleErrors)

但這個(gè)處理方法并不全面,如果handleErrors中有錯(cuò),promise中的錯(cuò)誤也不能被成功捕獲。

還有一種解決方法是,設(shè)置一個(gè)定時(shí)器,在拒絕的時(shí)候啟動(dòng),如果promise被拒絕,而在定時(shí)器出發(fā)之前都沒(méi)有出錯(cuò)處理函數(shù)被注冊(cè),呢么他就不會(huì)注冊(cè)處理韓式,進(jìn)而就是未被捕獲錯(cuò)誤。再多種庫(kù)中這個(gè)方法運(yùn)行良好,但設(shè)置定時(shí)時(shí)間太隨意了,如果處理某些請(qǐng)求真的pending了很長(zhǎng)時(shí)間,這個(gè)方法顯得并不那么可靠。

promise模式

1.Promise.all([..])

多個(gè)任務(wù)完成后再繼續(xù)執(zhí)行更多操作。在promise鏈中,任意時(shí)刻都只能有一個(gè)異步任務(wù)正在執(zhí)行,想要同時(shí)執(zhí)行兩個(gè)或更多步驟(并行執(zhí)行),必須使用來(lái)創(chuàng)建。

門要等待兩個(gè)或更多并行/并發(fā)的任務(wù)都完成才能繼續(xù)。完成順序并不重要,但必須都得完成,門才能打開(kāi)并讓流程控制繼續(xù)。

Promise.all([p1,p2])
.then(msgs=>{
    // 這里的msg也是一個(gè)數(shù)組,分別為p1、p2的完成消息
})

需要注意的是,p1、p2中,如果有任何一個(gè)被拒絕的,主Promise.all()就會(huì)立即被拒絕,并丟棄來(lái)自其他所有promise的全部后果。

2.Promise.race([])

這個(gè)api指的是競(jìng)態(tài),傳統(tǒng)模式是稱為門閂,即只響應(yīng)第一個(gè)執(zhí)行完成的promise。

與Promise.all()相似,一旦有任何一個(gè)promise決議為完成,promise.race()就會(huì)完成;一旦有任何一個(gè)promise決議為拒絕,他就會(huì)拒絕。如果傳入了空數(shù)組,則永遠(yuǎn)不會(huì)被決議。

我們可以用它來(lái)檢測(cè)一個(gè)請(qǐng)求是否超時(shí):

Promise.race([
request(),
timeoutPromise()
])
.then(()=>{
    // request按時(shí)完成
},
err=>{
    // request()被拒絕或超時(shí)!!
}
)

并發(fā)迭代

有時(shí)候需要再一列promise中迭代,并對(duì)所有promise都執(zhí)行某個(gè)任務(wù)。其實(shí)就像是同步數(shù)組迭代,但改成了異步。使用map迭代promise(或其他任何值),再每個(gè)值上運(yùn)行一個(gè)函數(shù)作為參數(shù)。map本身返回一個(gè)promise,其完成值是一個(gè)數(shù)組,該數(shù)組保持映射順序,保存任務(wù)執(zhí)行之后的異步完成值:

Promise.map((vals,cb)=>{
    return Promise.all(
        vals.map(val=>
            new Promise(resolve=>cb(val,resolve))
        )
    )
})

在以上的map實(shí)現(xiàn)中,不能發(fā)送異步拒絕拒絕信號(hào),但如果在映射的回調(diào)cb中,出現(xiàn)同步的異?;蝈e(cuò)誤,主Promise.map()返回的promise就會(huì)拒絕。

Promise的局限性

promise看似神奇,解決了很多異步回調(diào)的問(wèn)題,使代碼清晰可靠。但萬(wàn)事沒(méi)有百分百完美的,promise也有自身的局限性。

1.順序錯(cuò)誤處理

由于promise的鏈接方式,promise中的錯(cuò)誤很容易被無(wú)意中默默忽略掉。如果構(gòu)建了一個(gè)沒(méi)有錯(cuò)誤處理的promise鏈,鏈中任何地方都會(huì)在鏈中一直傳遞下去,直到被查看(通過(guò)在某個(gè)步驟注冊(cè)拒絕處理函數(shù))。

2.單一值

promise只能有一個(gè)完成值或一個(gè)拒絕理由。在簡(jiǎn)單的應(yīng)用中,這不是什么問(wèn)題;但是在復(fù)雜的場(chǎng)景中,你就會(huì)發(fā)現(xiàn)這是一種局限。
如果需要處理多個(gè)值,直接將多個(gè)值封裝成promise,并把它們放到數(shù)組中,使用Promise.all()來(lái)進(jìn)行處理。

3.單決議

promise只能被決議一次,如果將某個(gè)事件綁定,放到promise中,如下例。如果按鈕響應(yīng)只點(diǎn)擊一次,這種方式才能運(yùn)作。點(diǎn)擊第二次,promise已經(jīng)決議,所以第二次調(diào)用resolve()就會(huì)被忽略。如下例,只會(huì)再第一次點(diǎn)擊時(shí)打印50,之后的點(diǎn)擊resolve被忽略。

function click(ele,event){
    document.getElementById(ele).onclick=event
}

const request=()=>new Promise((resolve,rej)=>{
    resolve(50) 
})

const p=new Promise((resolve,reject)=>{
    click('test',resolve)
})

p.then(evt=>request())
.then(text=>{console.log(text)})

4.無(wú)法取消的promise

如果建立了一個(gè)promise并為其注冊(cè)了完成或拒絕處理函數(shù),如果出現(xiàn)某種情況使這個(gè)任務(wù)懸而未決的話,也沒(méi)有辦法從外部停止他的進(jìn)程。

考慮前面的超時(shí)場(chǎng)景:使用Promise.race()設(shè)置一個(gè)定時(shí)器,到時(shí)則拋出錯(cuò)誤。

5.promise的性能問(wèn)題

promise做的工作比自身建立的回調(diào)方案,要慢一些。但promise值得信任,損失微小的的性能但能讓整個(gè)系統(tǒng)可信任性和組合性更高;代碼條理也更加清晰。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容