2017年11月23日 星期四

JavaScript Promises 非同步程式簡介 JavaScript Promises for asynchronous programming

 

Promises 非同步程式開發筆記,前端的東西真的很多歷史共業呀,Promise 算是比較現代化的作法囉XD

圖:RYAN SUKALE,  JavaScript Promises — Visualized




一、Promise 簡介


Promise 結構是一種非同步執行的控制流程架構,在 ES6 被提出,這種結構解決了 ES5 以前 callback hell (callback 程式碼排版一直縮排)。ES7 有新的語法 async 和 await,更接近傳統同步程式的寫法,但背後原理還是建立在 Promise 的基礎上,所以還是需要理解 Promise 的 : )


callback hell 示意圖
(from Silvana Goberdhan-Vigle, Promises)




二、Promise 物件狀態


Promise 物件必定是以下三種狀態中的其中一種:

  1. Pending (等待中):Promise 的初始狀態, 非同步的結果還沒計算完成。
  2. Fulfilled (已實現):執行成功。
  3. Rejected (已拒絕):執行時發生錯誤。

* settled:執行完成 (Fulfilled 或 Rejected 皆是 settled)


Promise 物件狀態示意圖
(from TypeScript Deep Dive)




三、Promise 語法


Promise 基本語法如下,多數實作情況會使用別人構造的 promise-based 物件而不用自定義 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 這種寫法的主要原因有以下三者:

  1. Revealing Constructor Pattern
  2. 封裝 (Promise 物件不外露狀態)
  3. 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 函式回傳值可以有以下不同類型:

  1. promise 物件:最一般的非同步方式
  2. synchronous value:會自動轉成 promise 物件
  3. synchronous error:錯誤處理


[用心去感覺] then 方法的使用細節

  1. Be careful: non-returning functions in JavaScript technically return undefined (synchronous value --> promise)
  2. 其實 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 異步生活
https://www.gitbook.com/book/eyesofkids/javascript-start-es6-promise/details

Exploring ES6 - Promises for asynchronous programming
技術提供:Blogger.