Promise

在學(xué)習(xí)本章節(jié)內(nèi)容前,你需要先了解什么是異步編程,可以參考:JavaScript 異步編程

Promise 是一個(gè) ECMAScript 6 提供的類,目的是更加優(yōu)雅地書(shū)寫復(fù)雜的異步任務(wù)。

由于 Promise 是 ES6 新增加的,所以一些舊的瀏覽器并不支持,Promise 對(duì)象代表了未來(lái)將要發(fā)生的事件,用來(lái)傳遞異步操作的消息。

構(gòu)造 Promise

現(xiàn)在我們新建一個(gè) Promise 對(duì)象:

Promise 構(gòu)造函數(shù)包含一個(gè)參數(shù)和一個(gè)帶有 resolve(解析)和 reject(拒絕)兩個(gè)參數(shù)的回調(diào)。在回調(diào)中執(zhí)行一些操作(例如異步),如果一切都正常,則調(diào)用 resolve,否則調(diào)用 reject。

new Promise(function (resolve, reject) {
    // 要做的事情...(異步處理)
    // 處理結(jié)束后、調(diào)用resolve 或 reject
});

通過(guò)新建一個(gè) Promise 對(duì)象好像并沒(méi)有看出它怎樣 "更加優(yōu)雅地書(shū)寫復(fù)雜的異步任務(wù)"。我們之前遇到的異步任務(wù)都是一次異步,如果需要多次調(diào)用異步函數(shù)呢?例如,如果我想分三次輸出字符串,第一次間隔 1 秒,第二次間隔 4 秒,第三次間隔 3 秒:

setTimeout(function () {
    console.log("First");
    setTimeout(function () {
        console.log("Second");
        setTimeout(function () {
            console.log("Third");
        }, 3000);
    }, 4000);
}, 1000);

//First
//Second
//Third

這段程序?qū)崿F(xiàn)了這個(gè)功能,但是它是用 "函數(shù)瀑布" 來(lái)實(shí)現(xiàn)的??上攵?,在一個(gè)復(fù)雜的程序當(dāng)中,用 "函數(shù)瀑布" 實(shí)現(xiàn)的程序無(wú)論是維護(hù)還是異常處理都是一件特別繁瑣的事情,而且會(huì)讓縮進(jìn)格式變得非常冗贅。

現(xiàn)在我們用 Promise 來(lái)實(shí)現(xiàn)同樣的功能:

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

//First
//Second
//Third

這段代碼較長(zhǎng),所以還不需要完全理解它,我想引起注意的是 Promise 將嵌套格式的代碼變成了順序格式的代碼。

Promise 的使用

下面我們通過(guò)剖析這段 Promise "計(jì)時(shí)器" 代碼來(lái)講述 Promise 的使用:

Promise 構(gòu)造函數(shù)只有一個(gè)參數(shù),是一個(gè)函數(shù),這個(gè)函數(shù)在構(gòu)造之后會(huì)直接被異步運(yùn)行,所以我們稱之為起始函數(shù)。起始函數(shù)包含兩個(gè)參數(shù) resolve 和 reject。

當(dāng) Promise 被構(gòu)造時(shí),起始函數(shù)會(huì)被異步執(zhí)行:

new Promise(function (resolve, reject) {
    console.log("Run");
});

這段程序會(huì)直接輸出 Run。

resolve 和 reject 都是函數(shù),其中調(diào)用 resolve 代表一切正常,reject 是出現(xiàn)異常時(shí)所調(diào)用的:

new Promise(function (resolve, reject) {
    var a = 0;
    var b = 1;
    if (b == 0) reject("Divide zero");
    else resolve(a / b);
}).then(function (value) {
    console.log("a / b = " + value);
}).catch(function (err) {
    console.log(err);
}).finally(function () {
    console.log("End");
});

這段程序執(zhí)行結(jié)果是:

a / b = 0
End

Promise 類有.then() .catch().finally()三個(gè)方法,這三個(gè)方法的參數(shù)都是一個(gè)函數(shù),.then() 可以將參數(shù)中的函數(shù)添加到當(dāng)前 Promise 的正常執(zhí)行序列,.catch() 則是設(shè)定 Promise 的異常處理序列,.finally() 是在 Promise 執(zhí)行的最后一定會(huì)執(zhí)行的序列。 .then() 傳入的函數(shù)會(huì)按順序依次執(zhí)行,有任何異常都會(huì)直接跳到 catch 序列:

new Promise(function (resolve, reject) {
    console.log(1111); 
    resolve(2222);  //輸出2222數(shù)據(jù)
}).then(function (value) {
    console.log(value);  //打印2222數(shù)據(jù)
    return 3333;  //輸出3333數(shù)據(jù)
}).then(function (value) {
    console.log(value); //打印3333數(shù)據(jù)
    throw "An error";  //拋出錯(cuò)誤數(shù)據(jù),終止后續(xù) then 操作
}).catch(function (err) {
    console.log(err);  //打印錯(cuò)誤數(shù)據(jù)
});

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

1111
2222
3333
An error

resolve() 中可以放置一個(gè)參數(shù)用于向下一個(gè) then 傳遞一個(gè)值,then 中的函數(shù)也可以返回一個(gè)值傳遞給 then。但是,如果 then 中返回的是一個(gè) Promise 對(duì)象,那么下一個(gè) then 將相當(dāng)于對(duì)這個(gè)返回的 Promise 進(jìn)行操作,這一點(diǎn)從剛才的計(jì)時(shí)器的例子中可以看出來(lái)。

reject() 參數(shù)中一般會(huì)傳遞一個(gè)異常給之后的 catch 函數(shù)用于處理異常。

比較下then 中返回的是對(duì)象 / 值

then 中返回的是一個(gè) Promise 對(duì)象

如果返回的是Promise 對(duì)象,第一個(gè)回調(diào)函數(shù)完成以后,會(huì)將返回結(jié)果作為參數(shù),傳入第二個(gè)回調(diào)函數(shù)。
如果前一個(gè)回調(diào)函數(shù)返回的是Promise對(duì)象,這時(shí)后一個(gè)回調(diào)函數(shù)就會(huì)等待該P(yáng)romise對(duì)象有了運(yùn)行結(jié)果,才會(huì)進(jìn)一步調(diào)用。(獲取結(jié)果順序不變(相當(dāng)于同步編程))

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve();
    }, 1000);
}).then(function () {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log("Second");
            resolve();
        }, 4000);
    });
}).then(function () {
    setTimeout(function () {
        console.log("Third");
    }, 3000);
});

//First
//Second
//Third
then 中返回的是一個(gè)值

如果返回的是值,獲取結(jié)果順序?qū)⒏鶕?jù) 時(shí)間先后 排列, 而且第二個(gè)then無(wú)法獲取Promise數(shù)據(jù),只能獲取上一個(gè)then中返回的數(shù)據(jù),沒(méi)有則返回undefined

new Promise(function (resolve, reject) {
    setTimeout(function () {
        console.log("First");
        resolve("Second")
    }, 1000);
}).then(function (value) {  //第一個(gè)then
        setTimeout(function () {
            console.log(value);
        }, 3000);
        return "Third"
}).then(function (value) {  //第二個(gè)then
    setTimeout(function () {
        console.log(value);
    }, 2000);
});

//First
//Third
//Second

但是請(qǐng)注意以下兩點(diǎn):

  • resolve 和 reject 的作用域只有起始函數(shù),不包括 then 以及其他序列;
  • resolve 和 reject 并不能夠使起始函數(shù)停止運(yùn)行,別忘了 return。

Promise 函數(shù)

上述的 "計(jì)時(shí)器" 程序看上去比函數(shù)瀑布還要長(zhǎng),所以我們可以將它的核心部分寫成一個(gè) Promise 函數(shù):

//有setTimeout
function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

//無(wú)setTimeout
function print( message) {
    return new Promise(function (resolve, reject) {
            console.log(message);
            resolve();
    });
}

然后我們就可以放心大膽的實(shí)現(xiàn)程序功能了:

//有setTimeout
print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});

//無(wú)setTimeout
print("First").then(function () {
    return print("Second");
}).then(function () {
    print("Third");
});

這種返回值為一個(gè) Promise 對(duì)象的函數(shù)稱作 Promise 函數(shù),它常常用于開(kāi)發(fā)基于異步操作的庫(kù)。

回答常見(jiàn)的問(wèn)題(FAQ)

Q: then、catch 和 finally 序列能否順序顛倒?

A: 可以,效果完全一樣。但不建議這樣做,最好按 then-catch-finally 的順序編寫程序。

Q: 除了 then 塊以外,其它兩種塊能否多次使用?

A: 可以,finally 與 then 一樣會(huì)按順序執(zhí)行,但是 catch 塊只會(huì)執(zhí)行第一個(gè),除非 catch 塊里有異常。所以最好只安排一個(gè) catch 和 finally 塊。

Q: then 塊如何中斷?

A: then 塊默認(rèn)會(huì)向下順序執(zhí)行,return 是不能中斷的,可以通過(guò) throw 來(lái)跳轉(zhuǎn)至 catch 實(shí)現(xiàn)中斷。

Q: 什么時(shí)候適合用 Promise 而不是傳統(tǒng)回調(diào)函數(shù)?

A: 當(dāng)需要多次順序執(zhí)行異步操作的時(shí)候,例如,如果想通過(guò)異步方法先后檢測(cè)用戶名和密碼,需要先異步檢測(cè)用戶名,然后再異步檢測(cè)密碼的情況下就很適合 Promise。

Q: Promise 是一種將異步轉(zhuǎn)換為同步的方法嗎?

A: 完全不是。Promise 只不過(guò)是一種更良好的編程風(fēng)格。

Q: 什么時(shí)候我們需要再寫一個(gè) then 而不是在當(dāng)前的 then 接著編程?

A: 當(dāng)你又需要調(diào)用一個(gè)異步任務(wù)的時(shí)候。

異步函數(shù)

異步函數(shù)(async function)是 ECMAScript 2017 (ECMA-262) 標(biāo)準(zhǔn)的規(guī)范,幾乎被所有瀏覽器所支持,除了 Internet Explorer。

在 Promise 中我們編寫過(guò)一個(gè) Promise 函數(shù):

function print(delay, message) {
    return new Promise(function (resolve, reject) {
        setTimeout(function () {
            console.log(message);
            resolve();
        }, delay);
    });
}

然后用不同的時(shí)間間隔輸出了三行文本:

print(1000, "First").then(function () {
    return print(4000, "Second");
}).then(function () {
    print(3000, "Third");
});

我們可以將這段代碼變得更好看:async / await

async function asyncFunc() {
    await print(1000, "First");
    await print(4000, "Second");
    await print(3000, "Third");
}
asyncFunc();

//First
//Second
//Third

哈!這豈不是將異步操作變得像同步操作一樣容易了嗎!

這次的回答是肯定的,異步函數(shù) async function 中可以使用 await 指令,await 指令后必須跟著一個(gè) Promise,異步函數(shù)會(huì)在這個(gè) Promise 運(yùn)行中暫停,直到其運(yùn)行結(jié)束再繼續(xù)運(yùn)行。

異步函數(shù)實(shí)際上原理與 Promise 原生 API 的機(jī)制是一模一樣的,只不過(guò)更便于程序員閱讀。

處理異常的機(jī)制將用 try-catch 塊實(shí)現(xiàn):

async function asyncFunc() {
    try{
        await print(1000, "First");
        await print(4000, "Second");
        await print(3000, "Third");
        throw "error"; 
    }catch(err){
        console.log(err)
    }

}
asyncFunc();

//First
//Second
//error

如果 Promise 有一個(gè)正常的返回值,await 語(yǔ)句也會(huì)返回它:

async function asyncFunc() {
    let value = await new Promise(
        function (resolve, reject) {
            resolve("Return value");
        }
    );
    console.log(value);
}
asyncFunc();

程序會(huì)輸出:

Return value

Promise 對(duì)象有以下兩個(gè)特點(diǎn):

1、對(duì)象的狀態(tài)不受外界影響。Promise 對(duì)象代表一個(gè)異步操作,有三種狀態(tài):

pending: 初始狀態(tài),不是成功或失敗狀態(tài)。
fulfilled: 意味著操作成功完成。
rejected: 意味著操作失敗。
只有異步操作的結(jié)果,可以決定當(dāng)前是哪一種狀態(tài),任何其他操作都無(wú)法改變這個(gè)狀態(tài)。這也是 Promise 這個(gè)名字的由來(lái),它的英語(yǔ)意思就是「承諾」,表示其他手段無(wú)法改變。

2、一旦狀態(tài)改變,就不會(huì)再變,任何時(shí)候都可以得到這個(gè)結(jié)果。Promise 對(duì)象的狀態(tài)改變,只有兩種可能:從 Pending 變?yōu)?Resolved 和從 Pending 變?yōu)?Rejected。只要這兩種情況發(fā)生,狀態(tài)就凝固了,不會(huì)再變了,會(huì)一直保持這個(gè)結(jié)果。就算改變已經(jīng)發(fā)生了,你再對(duì) Promise 對(duì)象添加回調(diào)函數(shù),也會(huì)立即得到這個(gè)結(jié)果。這與事件(Event)完全不同,事件的特點(diǎn)是,如果你錯(cuò)過(guò)了它,再去監(jiān)聽(tīng),是得不到結(jié)果的。

Promise 優(yōu)缺點(diǎn)

有了 Promise 對(duì)象,就可以將異步操作以同步操作的流程表達(dá)出來(lái),避免了層層嵌套的回調(diào)函數(shù)。此外,Promise 對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易。

Promise 也有一些缺點(diǎn)。首先,無(wú)法取消 Promise,一旦新建它就會(huì)立即執(zhí)行,無(wú)法中途取消。其次,如果不設(shè)置回調(diào)函數(shù),Promise 內(nèi)部拋出的錯(cuò)誤,不會(huì)反應(yīng)到外部。第三,當(dāng)處于 Pending 狀態(tài)時(shí),無(wú)法得知目前進(jìn)展到哪一個(gè)階段(剛剛開(kāi)始還是即將完成)。

【筆記】

Promise 對(duì)象代表一個(gè)異步操作,有三種狀態(tài):Pending(進(jìn)行中)、Resolved(已完成,又稱 Fulfilled)和 Rejected(已失敗)。

通過(guò)回調(diào)里的 resolve(data) 將這個(gè) promise 標(biāo)記為 resolverd,然后進(jìn)行下一步 then((data)=>{//do something}),resolve 里的參數(shù)就是你要傳入 then 的數(shù)據(jù)。

?著作權(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)容