JavaScript Promise

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 物件