圖:RYAN SUKALE, JavaScript Promises — Visualized
一、Promise 簡介
Promise 結構是一種非同步執行的控制流程架構,在 ES6 被提出,這種結構解決了 ES5 以前 callback hell (callback 程式碼排版一直縮排)。ES7 有新的語法 async 和 await,更接近傳統同步程式的寫法,但背後原理還是建立在 Promise 的基礎上,所以還是需要理解 Promise 的 : )
callback hell 示意圖
(from Silvana Goberdhan-Vigle, Promises)
(from Silvana Goberdhan-Vigle, Promises)
二、Promise 物件狀態
Promise 物件必定是以下三種狀態中的其中一種:
- Pending (等待中):Promise 的初始狀態, 非同步的結果還沒計算完成。
- Fulfilled (已實現):執行成功。
- Rejected (已拒絕):執行時發生錯誤。
* settled:執行完成 (Fulfilled 或 Rejected 皆是 settled)
Promise 物件狀態示意圖
(from TypeScript Deep Dive)
(from TypeScript Deep Dive)
三、Promise 語法
var p = new Promise(function(resolve, reject) { // Do async tasks if(/* good condition */) { resolve('Success'); /* result */ } else { reject('Failure'); } }); p.then(function() { /* do something with the result */ }).then(function() { /* do something with the result */ }).then(function() { /* do something with the result */ }).catch(function() { /* error */ })
Executor
new Promise() 時使用的參數稱為 executor.
- Resolving: 如果順利執行,executor 會把結果送給 resolve()。
- Rejecting: 如果發生錯誤,executor 會把錯誤送給 reject()。
使用 executor 這種寫法的主要原因有以下三者:
- Revealing Constructor Pattern
- 封裝 (Promise 物件不外露狀態)
- Throw safety
實際使用時不知道這些細節也影響不大,這是設計 Promise 上的議題,欲知詳情可以看文末的 references。
四、Promise 的方法:then 和 catch
1. then 方法 [重要!]
其實整個 Promise 結構得以運作,最重要的地方就是這個 Promise 的 then 方法:
promise2 = promise1.then(onFulfilled, onRejected)
這個方法會從 resolve() 或 reject() 得到結果傳入 onFulfilled() 或 onRejected() 中,且 then 被規定必須回傳一個 promise。因此,這讓 then() 得以進行連鎖呼叫,避免了 Callback hell。
實際上,then 函式回傳值可以有以下不同類型:
- promise 物件:最一般的非同步方式
- synchronous value:會自動轉成 promise 物件
- synchronous error:錯誤處理
[用心去感覺] then 方法的使用細節
- Be careful: non-returning functions in JavaScript technically return undefined (synchronous value --> promise)
- 其實 thenable 物件也會自動轉成 promise 物件,thenable 意思就是有 "then" 方法的物件,沒什麼特別的地方,就方便轉換成 Promise 而已。
下面是一個混合不同種回傳值的登錄實作:
getUserByName('nolan').then(function (user) { if (user.isLoggedOut()) { throw new Error('user logged out!'); /* throwing a synchronous error */ } if (inMemoryCache[user.id]) { return inMemoryCache[user.id]; /* returning a synchronous value */ } return getUserAccountById(user.id); /* returning a promise */ }) .then(function (userAccount) { /* got a user account */ }) .catch(function (err) { /* got an error */ });
2. catch 方法
當 promise 物件 rejected 時執行 catch 方法 ,其實 catch 方法正是 then(null, ...) 的語法糖。
promise.then( null, error => { /* rejection */ }); promise.catch( error => { /* rejection */ });
如果有一連串的 then 方法其中一個發生錯誤,則沒被接住的錯誤會向下傳遞,直到有 error handler 幫忙接住處理。如果沒寫 catch 錯誤會直接 swallow 不易 debug,所以最好要有寫 catch 的習慣。
asyncFunc1() .then(asyncFunc2) .then(asyncFunc3) .catch(function (reason) { // Something went wrong above });
注意各個 then 方法間參數和錯誤傳遞的方式,參數只會拿上一家的,錯誤則會上面全接。
const p1 = new Promise((resolve, reject) => { resolve(1) }) p1.then((val) => { console.log(val) //1 return val + 2 }) .then((val) => { console.log(val) //3 throw new Error('error!') }) .catch((err) => { console.log(err.message) //return 4 }) .then((val) => console.log(val, 'done')) //val undefined
五、Promise 的靜態方法
1. Promise.resolve() 和 Promise.reject() 靜態方法
Promises/A+ 中沒有 Promise.reject / Promise.resolve 的定義,這是 ES6 Promise 中的實作。
- Promise.resolve 會直接產生 fulfilled 狀態的 Promise 物件
- Promise.reject 會直接產生 rejected 狀態的 Promise 物件。
Promise.reject 和 Promise.resolve 最好只用於非 Promise 物件轉換 Promise 物件的地方 (可以轉換 Promise、thenable 物件或一般值);不要寫成一般 function 裡有這兩種方法的樣子。
2. Promise.all() 和 Promise.race() 靜態方法
Promise.all() 是平行運算時使用的靜態方法,可以改寫多數想使用 forEach() 的使用場景,所有的 promise 物件都返回 fullfilled 才會傳入 then() (像 AND 的行為)。
- 陣列元素順序與執行順序無關
- 陣列中的值如果不是 Promise 物件,會使用 Promise.resolve 轉換。
- 只要有其中一個陣列中的 Promise 物件發生錯誤或 reject ,會立即回傳一個 rejected 狀態 promise 物件。
- 完成後會得到各個回傳 Promise 的陣列。
Promise.race () 和 Promise.all() 相似 (像 OR 的行為) ,任何一個陣列傳入參數的 Promise 物件解決,便會往下執行。
Example: map() via Promise.all()
const fileUrls = [ 'http://example.com/file1.txt', 'http://example.com/file2.txt', ]; const promisedTexts = fileUrls.map(httpGet); Promise.all(promisedTexts) .then(texts => { for (const text of texts) { console.log(text); } }) .catch(reason => { // Receive first reject });
六、ES7 的新語法:async 和 await
ES7 引進的 async/await 是建立在 Promises 的基礎上 (Async functions 無論有沒有使用 await 一定會回傳 promise)。對未學過非同步程式的人來說可讀性較佳,學習曲線較為平緩。
Example: Logging a fetch (promises)
function logFetch(url) { return fetch(url) .then( response => response.text() ) .then( text => { console.log(text); }) .catch(err => { console.error( 'fetch failed', err ); }
Example: Logging a fetch (async)
async function logFetch(url) { try { const response = await fetch(url); console.log(await response.text()); } catch (err) { console.log('fetch failed', err); } }
七、補充議題
1. 關於 Callback
在伺服器端的非同步需求 (如 Node.js) 遠遠大於瀏覽器端。瀏覽器只有 AJAX、DOM 事件、動畫處理等地方會用到非同步的設計方式。
- 同步執行函式的結果要不就是回傳一個值,要不然就是執行到一半發生例外,中斷目前的程式然後拋出例外。
- 非同步執行函式的結果是帶有回傳值的成功,或回傳理由的失敗。
2. Promise 外部函式庫議題
常見實作版本:
- q: 維護停止,最後發佈版本在 2015/5。
- bluebird: 維護持續(唯一),最後發佈版本在 2016/6。
- when: 維護停止,最後發佈版本在 2015/12。
- then-promise: 維護停止,最後發佈版本在 2015/12。
- rsvp.js: 維護停止,最後發佈版本在 2016/2。
- vow: 維護停止,最後發佈版本在 2015/12。
通常建議 (安全考量):
- 伺服器端 (Node.js) 使用 bluebird
- 瀏覽器端使用原生 ES6 Promise 或 Q
- pollyfill 是好物!
3. Can I use promises?
可用 promise 瀏覽器一覽表 : https://caniuse.com/#feat=promises
2017 支援分佈:
- Taiwan: 86.86% + 0.02% = 86.89%
- Global: 89.13% + 0.03% = 89.15%
- IE 8, 11 不支援
4. Other issue
then 的奇葩使用:網路上有人在討論各種奇葩 then 方法的使用情況,但總而言之,then 方法的傳入參數最好是 (1) 函式 或 (2) 有一個回傳值的匿名函式,這樣就不會有問題。
歷史共業 Defer:古代還沒有 Promise 時是使用 Defer-based 的方法 (如 jQuery 和 Angular js),但現在這些方法也大多支援 Promise-based 的寫法了 (wrap 過去之類的),所以直接學 Promise 吧!
References
Promises/A+ specification
https://promisesaplus.com/
從 Promise 開始的 JavaScript 異步生活
從 Promise 開始的 JavaScript 異步生活
https://www.gitbook.com/book/eyesofkids/javascript-start-es6-promise/details
Exploring ES6 - Promises for asynchronous programming
Exploring ES6 - Promises for asynchronous programming
https://github.com/wbinnssmith/awesome-promises
Jake Archibald - Async functions - making promises friendly
https://developers.google.com/web/fundamentals/primers/async-functions
David Walsh - JavaScript Promise API
https://davidwalsh.name/promises
We have a problem with promises
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
promise-fun
https://github.com/sindresorhus/promise-fun
Jake Archibald - Async functions - making promises friendly
https://developers.google.com/web/fundamentals/primers/async-functions
David Walsh - JavaScript Promise API
https://davidwalsh.name/promises
We have a problem with promises
https://pouchdb.com/2015/05/18/we-have-a-problem-with-promises.html
promise-fun
https://github.com/sindresorhus/promise-fun