JavaScript Promise

在學習本章節內容前,你需要先了解什麼是異步編程,可以參考:JavaScript 異步編程

Promise 是一個 ECMAScript 6 提供的類,目的是更加優雅地書寫複雜的異步任務。

由於 Promise 是 ES6 新增加的,所以一些舊的瀏覽器並不支持,蘋果的 Safari 10 和 Windows 的 Edge 14 版本以上瀏覽器才開始支持 ES6 特性。

以下是 Promise 瀏覽器支持的情況:

Chrome 58 Edge 14 Firefox 54 Safari 10 Opera 55

構造 Promise

現在我們新建一個 Promise 對象:

new Promise(function (resolve, reject) {
    // 要做的事情...
});

通過新建一個 Promise 對象好像並沒有看出它怎樣 "更加優雅地書寫複雜的異步任務"。我們之前遇到的異步任務都是一次異步,如果需要多次調用異步函數呢?例如,如果我想分三次輸出字符串,第一次間隔 1 秒,第二次間隔 4 秒,第三次間隔 3 秒:

實例

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

這段程序實現了這個功能,但是它是用 "函數瀑布" 來實現的。可想而知,在一個複雜的程序當中,用 "函數瀑布" 實現的程序無論是維護還是異常處理都是一件特別繁瑣的事情,而且會讓縮進格式變得非常冗贅。

現在我們用 Promise 來實現同樣的功能:

實例

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); });

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

Promise 的使用

下麵我們通過剖析這段 Promise "計時器" 代碼來講述 Promise 的使用:

Promise 構造函數隻有一個參數,是一個函數,這個函數在構造之後會直接被異步運行,所以我們稱之為起始函數。起始函數包含兩個參數 resolve 和 reject。

當 Promise 被構造時,起始函數會被異步執行:

實例

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

這段程序會直接輸出 Run

resolve 和 reject 都是函數,其中調用 resolve 代表一切正常,reject 是出現異常時所調用的:

實例

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"); });

這段程序執行結果是:

a / b = 0
End

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

實例

new Promise(function (resolve, reject) { console.log(1111); resolve(2222); }).then(function (value) { console.log(value); return 3333; }).then(function (value) { console.log(value); throw "An error"; }).catch(function (err) { console.log(err); });

執行結果:

1111
2222
3333
An error

resolve() 中可以放置一個參數用於向下一個 then 傳遞一個值,then 中的函數也可以返回一個值傳遞給 then。但是,如果 then 中返回的是一個 Promise 對象,那麼下一個 then 將相當於對這個返回的 Promise 進行操作,這一點從剛才的計時器的例子中可以看出來。

reject() 參數中一般會傳遞一個異常給之後的 catch 函數用於處理異常。

但是請注意以下兩點:

  • resolve 和 reject 的作用域隻有起始函數,不包括 then 以及其他序列;
  • resolve 和 reject 並不能夠使起始函數停止運行,別忘了 return。

Promise 函數

上述的 "計時器" 程序看上去比函數瀑布還要長,所以我們可以將它的核心部分寫成一個 Promise 函數:

實例

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

然後我們就可以放心大膽的實現程序功能了:

實例

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

這種返回值為一個 Promise 對象的函數稱作 Promise 函數,它常常用於開發基於異步操作的庫。

回答常見的問題(FAQ)

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

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

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

A: 可以,finally 與 then 一樣會按順序執行,但是 catch 塊隻會執行第一個,除非 catch 塊裏有異常。所以最好隻安排一個 catch 和 finally 塊。

Q: then 塊如何中斷?

A: then 塊默認會向下順序執行,return 是不能中斷的,可以通過 throw 來跳轉至 catch 實現中斷。

Q: 什麼時候適合用 Promise 而不是傳統回調函數?

A: 當需要多次順序執行異步操作的時候,例如,如果想通過異步方法先後檢測用戶名和密碼,需要先異步檢測用戶名,然後再異步檢測密碼的情況下就很適合 Promise。

Q: Promise 是一種將異步轉換為同步的方法嗎?

A: 完全不是。Promise 隻不過是一種更良好的編程風格。

Q: 什麼時候我們需要再寫一個 then 而不是在當前的 then 接著編程?

A: 當你又需要調用一個異步任務的時候。

異步函數

異步函數(async function)是 ECMAScript 2017 (ECMA-262) 標準的規範,幾乎被所有瀏覽器所支持,除了 Internet Explorer。

在 Promise 中我們編寫過一個 Promise 函數:

實例

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

然後用不同的時間間隔輸出了三行文本:

實例

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

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

實例

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

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

這次的回答是肯定的,異步函數 async function 中可以使用 await 指令,await 指令後必須跟著一個 Promise,異步函數會在這個 Promise 運行中暫停,直到其運行結束再繼續運行。

異步函數實際上原理與 Promise 原生 API 的機製是一模一樣的,隻不過更便於程序員閱讀。

處理異常的機製將用 try-catch 塊實現:

實例

async function asyncFunc() { try { await new Promise(function (resolve, reject) { throw "Some error"; // 或者 reject("Some error") }); } catch (err) { console.log(err); // 會輸出 Some error } } asyncFunc();

如果 Promise 有一個正常的返回值,await 語句也會返回它:

實例

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

程序會輸出:

Return value

更多內容

JavaScript Promise 對象