同步非同步

一般來說 JavaScript 裡面有分成同步 sync 和非同步 async,在同步模式下,每個任務必須按照順序執行,後面的任務必須等待前面的任務執行完成,非同步模式則相反,後面的任務不用等前面的,各自執行各自的任務,例如setTimeoutsetInterval都是這種模式。

延伸閱讀:非同步(Asynchronous)與同步(Synchronous)的差異

Promise

為瞭解決同步非同步的問題,我開始學習 Promise,剛接觸 Promise 的時候實在是有點頭昏腦脹,因為網路上的資料很多,但總找不到一個合適的範例展示,導致一直沒辦法深入理解,後來找到了下面這兩篇相當完整且豐富的教學,也讓我終於踏入了 Promise 的世界,由於這兩篇文章已經相當完整,就不在這邊做描述,有興趣的可以點進去看看。

簡單來說,Promise 就是「承諾」,可以想像成 A 承諾 B 要去辦事,辦完之後才會回報結果,而這個結果只有兩種狀況:成功與失敗,不會有處於成功失敗不明的中間狀況。

所以換成程式的講法,要使用 Promise,一開始要先 new 一個 Promise 物件,物件中的建構式包含兩個參數:resolve ( 成功 ) 與 reject ( 失敗 )

基本創建 Promise 的方法如下,resolve 在函式或流程成功,或有合法值的狀況下會執行,reject 則是在失敗或有錯誤的時候會執行,resolve 和 reject 都有一個回傳值,可將這個會傳值透過.then傳給下一個流程。

function asyncFunction(value) {
  return new Promise(function(resolve, reject){
    if(value){
      resolve(value) // 已實現,成功
    }else{
      reject(reason) // 有錯誤,已拒絕,失敗
    }
  });
}

同步延遲

大概知道用法之後,先來個簡單的例子,建立一個 delay 的流程,流程裡有個setTimeout,在延遲一秒後,將 resolve 成功的值透過.then傳下去。

function delay() {   
  return new Promise(function (resolve, reject) {
    setTimeout(function () {
      resolve('我是傳下去的值');
    }, 1000);
  });
}

delay().then(function (value) {
  console.log(value);    // '我是傳下去的值'
}).catch(function (error) {
  console.log(error);
});

理解 Promise

Promise 簡單來說:

想像一下你是個孩子,你媽承諾(Promise)你下個禮拜會送你一隻新手機。

現在你並不知道下個禮拜你會不會拿到手機。你媽可能真的買了新手機給你,或者因為你惹她不開心而取消了這個承諾。

這就是一個Promise,一個 Promise 有三種狀態:

  1. pending 未發生、等待的狀態。到下週前,你還不知道這件事會怎樣。
  2. resolved 完成/履行承諾。你媽真的買了手機給你。
  3. rejected 拒絕承諾。沒收到手機,因為你惹她不開心而取消了這個承諾。

總結來說狀態有等待成功失敗,使用情境就是我們當前還不知道結果,需要等待結果發生才繼續後續的處理

建立一個 Promise

看完上面的比喻讓我們對應到 Javascript。

var isMomHappy = false
var willIGetNewPhone = new Promise(function (resolve, reject) {
  if (isMomHappy) {
    var phone = {
      brand: 'Samsung',
      color: 'black',
      type: 's8'
    }
    resolve(phone)
  } else {
    var reason = new Error('Mom is unhappy')
    reject(reason)
  }
})

上面這段程式碼應該已經充分解釋概略的觀念。

  • 第一行我們使用一個 BooleanisMomHappy定義媽媽是否開心。
  • 我們宣告一個 PromisewillIGetNewPhone 這個 Promise 可能是被履行(resolved)又或者拒絕(rejected)
  • Promise 標準的語法可以參考 MDN
new Promise(function (resolve, reject) {})

我們需要記得的是如果一個 Promise 執行成功要在內部 function 呼叫 resolve(成功結果),如果結果是失敗則呼叫 reject(失敗結果)。在我們的範例中如果媽媽開心,我們將得到手機因此我們執行 reslove(phone),如果媽媽不高興則執行 reject(reason)。

一個 Promise 物件表達的是一件非同步的操作最終的結果,可以是成功或失敗。

使用 Promise

到這一步我們已經有了一個 Promise,讓我們接著來使用它。

var askMom = function () {
  willIGetNewPhone
    .then(function (fulfilled) {
      console.log(fulfilled)
    })
    .catch(function (error) {
      console.log(error.message)
    })
}
askMom()

首先我們有個 function 叫 askMom 在這個 function 中,我們將利用 Promise willIGetNewPhone。

我們希望一旦等待的結果發生時可以採取對應的動作,我們可以使用 .then 或 .catch 來執行對應的行為。

在這個範例中,我們在 .then 中使用 function (fulfilled){},而這個 fulfilled 就是從 Promise 的 resolve(成功結果) 傳來的結果,範例中這個結果就是 phone 物件。

在 .catch 中我們使用了 function (error) {}。而這個 error 就是從 Promise 的 reject(失敗結果) 傳來的即 reason。

鏈式調用 Promise

Promise 是可串連的。

假如你承諾您的朋友,如果你拿到新手機會借他們看看。這又是另一個Promise。讓我們繼續來撰寫這個範例:

var showOff = function (phone) {
  return new Promise(function (resolve, reject) {
    var message = 'Hey friend, I have a new ' + phone.color + ' ' + phone.brand + ' phone' + phone.type
    resolve(message) 
  })
}

在這個範例,您可能發現到我們根本沒有呼叫 reject,這是可選的,我們可以省略不調用。

另外,我們可以透過使用 Promise.resolve 簡化這個範例。

var askMom = function () {
  willIGetNewPhone
    .then(showOff)
    .then(function (fulfilled) {
      console.log(fulfilled)
      // 'Hey friend, I have a new black Samsung phone s8'
    })
    .catch(function (error) {
      console.log(error.message)
    })
}

這就是Promise串連的方式。

非同步

Promise是非同步的,讓我們在呼叫 Promise 的前後加上 console.log

var askMom = function () {
  console.log('before asking Mom')
  willIGetNewPhone
    .then(showOff)
    .then(function (fulfilled) {
      console.log(fulfilled)
    })
    .catch(function (error) {
      console.log(error.message)
    })

  console.log('after asking Mom')
}

關於上面這段程式碼,您認為 log 的順序會如何?大概我們會猜

1. before asking Mom
2. Hey friend, I have a new black Samsung phone s8
3. after asking Mom

然而真正的順序是

1. before asking Mom
2. after asking Mom
3. Hey friend, I have a new black Samsung phone s8

ES7

到了 ES7 更引進async await讓處理非同步的語法更加簡潔與具備可讀性。

const isMomHappy = true
const willIGetNewPhone = new Promise((resolve, reject) => {
  if (isMomHappy) {
    const phone = {
      brand: 'Samsung',
      color: 'black',
      type: 's8'
    }
    resolve(phone)
  } else {
    const reason = new Error('Mom is not happy')
    reject(reason)
  }
})
async function showOff(phone) {
  return Promise((resolve, reject) => {
    const message = 'Hey friend, I have a new ' + phone.color + ' ' + phone.brand + ' phone ' + phone.type
    return resolve(message)
  })
}
async function askMom () {
  try {
    console.log('before asking Mom')
    let phone = await willIGetNewPhone
    let message = await showOff(phone)
    console.log(message)
    console.log('after asking mom')
  } catch (error) {
    console.log(error.message)
  }
}
(async () => {
  await askMom()
})()
  • 當我們要在一個 function 中回傳 Promise 時,我們需要在該 function 前加上 async
  • 當我們使用一個 Promise 或者 function 回傳 Promise (async)時需要使用 await 例如 let phone = await willIGetNewPhone 或 let message = await showOff(phone)
  • 使用 try {} catch (error) {} 來攔截例外,即 Promise reject 時。

後起之秀 - Observables

在我們決定使用 Promise 之前,其實還有一個也是用來處理上面這些問題的新方法Observables

Observable 是 lazy event stream 可以觸發 0 到 多個事件,這些事件也不見得需要完成。

相比,Observable 和 Promise 相比,主要的差異為:

  • Observable 是可以被取消的。
  • Observable 在需要時才會執行。

下面是使用 RxJS 的範例

let Observable = Rx.Observable
let resultA, resultB, resultC
function addAsync (num1, num2) {
  const promise = fetch(`http://example.com?num1=${num1}&num2=${num2}`)
    .then(x => x.json())
  return Observable.fromPromise(promise)
}
addAsync(1, 2)
  .do(x => resultA = x)
  .flatMap(x => addAsync(x, 3))
  .do(x => resultB = x)
  .flatMap(x => addAsync(x, 4))
  .do(x => resultC = x)
  .subscribe(x => {
    console.log('total', x)
    conosle.log(resultA, resultB, resultC)
  })
  • Observable.fromPromise 會將 Promise 轉換成 observable stream。
  • .do 和 .flatMap 是 Observable 的運算子。
  • 所謂的需要時才執行(lazy)意思是當我們呼叫 .subscribe 時才會開始執行。
Observable 可以將一些複雜的處理流程變的容易。舉例來說我們可以只用一行程式碼去 delay 延遲相加函式 3 秒
addAsync(1, 2)
  .delay(3000)
  .do(x => resultA = x)

Promise物件的建立

ES6 Promise的實作中,會確保Promise物件一實體化後就會固定住狀態,要不就是"已實現",要不就是"已拒絕"

一個簡單的Promise語法結構如下:

const promise = new Promise(function(resolve, reject) {
  // 成功時
  resolve(value)
  // 失敗時
  reject(reason)
});

promise.then(function(value) {
  // on fulfillment(已實現時)
}, function(reason) {
  // on rejection(已拒絕時)
})

這個函式中又有兩個傳入參數值,resolve(解決)與reject(拒絕)都是要求一定是函式類型。成功的話,也就是有合法值的情況下執行resolve(value),promise物件的狀態會跑到fulfilled(已實現)固定住。失敗或是發生錯誤時用執行reject(reason),reason(理由)通常是用Error物件,然後promise物件的狀態會跑到rejected(已拒絕)狀態固定住。

這個建構函式的傳入函式稱為executor(執行者, 執行函式),這其實有強制執行的意味。這是一種名稱為暴露的建構式樣式 Revealing Constructor Pattern的樣式。executor會在建構式回傳物件實體前立即執行,也就是說當傳入這個函式時,Promise物件會立即決定裡面的狀態,看是要執行resolve來回傳值,還是要用reject來作錯誤處理。也因為它與一般的物件實體化的過程不太一樣,所以常會先包到一個函式中,使用時再呼叫這個函式來產生promise物件,例如像下面這樣的程式碼:

function asyncFunction(value) {
    return new Promise(function(resolve, reject){
        if(value)
            resolve(value) // 已實現,成功
        else
            reject(reason) // 有錯誤,已拒絕,失敗
    });
}
const promise = new Promise(function(resolveParam, rejectParam) {
    //resolveParam(1)
    rejectParam(new Error('error!'))
})

promise.then((value) => {
    console.log(value) // 1
    return value + 1
}).then((value) => {
    console.log(value) // 2
    return value + 2
}).catch((err) => console.log(err.message))

為什麼可以換傳入參數值的名稱?要回答這個問題,要先來解說一下進入Promise建構函式的大概執行流程,當然下面都是簡化過的說明:

  • 先用一個內部的物件,我把它稱為雛形物件,然後狀態(state)設定為pending(等待中),值(value)為undefined,理由(reason)為undefined
  • 再來初始化這個物件的工作,用init(promise, resolver)函式,傳入建構式中的傳入參數,也就是function(resolve, reject){...}當作resolver(解決者,解決用函式)傳入參數。
  • 把真正實作的Promise中的兩個內部_resolve函式,與_reject函式,對映到init(promise, resolver)中執行。
/內部用的雛形物件,實作上包含在建構式中用this
const pInternal = {
  state: 'pending',
  value: undefined,
  reason: undefined
}

//這個就是稱為executor的傳入參數
function resolver(resolve, reject){
  resolve(10)
  //reject(new Error('error occured !'))
}

//初始化內部雛形物件用的函式
function init(promise, resolver){
  try {
    resolver(function resolvePromise(value){
      _resolve(promise, value);
    }, function rejectPromise(reason) {
      _reject(promise, reason);
    })
  } catch(e) {
    _reject(promise, e);
  }

    return promise
}

//隱藏在內部的私有函式
function _resolve(promise, value){
  console.log(value)
  promise.state = 'onFulfilled'
  promise.value = value
}

//隱藏在內部的私有函式
function _reject(promise, reason){
  console.log(reason)
  promise.state = 'onRejected'
  promise.reason = reason
}

//最後生成回傳的promise物件
const promise = init(pInternal, resolver)
console.log(promise)

then與catch

then方法是Promise的最核心方法,標準有八成都是在定義then方法

在Promise的標準中,一直不斷的提到一個方法 -then,中文是"然後、接著、接下來"的意思,這個是一個Promise的重要方法。有定義then方法的物件被稱之為thenable物件,標準中花了一個章節在講then方法的規格,它的語法如下(出自MDN):

p.then(onFulfilled, onRejected);

p.then(function(value) {
   // fulfillment
  }, function(reason) {
  // rejection
});

then方法一樣用兩個函式當作傳入參數,onFulfilled是當promise物件的狀態轉變為fulfilled(已實現)呼叫的函式,有一個傳入參數值可用,就是value(值)。而onRejected是當promise物件的狀態轉變為rejected(已拒絕)呼叫的函式,也有一個傳入參數值可以用,就是reason(理由)。

為什麼說它是"一樣"?因為比對到promise物件的建構式,與那個傳入的函式參數值的樣子非常像,也是兩個函式當作傳入參數,只是名稱的定義上有點不同,但意義接近。

那麼then方法最後的回傳值是什麼?是另一個"新的"promise物件。

Promises/A+標準 2.2.7

then必須回傳一個promise。promise2 = promise1.then(onFulfilled, onRejected);

這樣設計的主要目的,是要能作連鎖(chained)的語法結構,也就是稱之為合成(composition)的一種運算方式,在JavaScript中如果回傳值相同的函式,可以使用連鎖的語法。像下面這樣的程式碼:

const promise = new Promise(function(resolve, reject) {
  resolve(1)
})

promise.then(function(value) {
  console.log(value) // 1
  return value + 1
}).then(function(value) {
  console.log(value) // 2
  return value + 2
}).then(function(value) {
  console.log(value) // 4
})

then方法中的onFulfilled函式,也就是第一個函式傳入參數,它是有值時使用的函式,經過連鎖的結構,如果要把值往下傳遞,可以用回傳值的方式,上面的例子可以看到用return語句來回傳值,這個值可以繼續的往下面的then方法傳送。

onRejected函式,也就是then方法中第二個函式的傳入參數,也有用回傳值往下傳遞的特性,不過因為它是使用於錯誤的處理,除非你是有要用來修正錯誤之類的理由,不然說實在這樣作會有點混亂,因為不論是onFulfilled函式或onRejected函式的傳遞值,都只會記錄到新產生的那個Promise物件之中,對"值"來說並沒有區分是onFulfilled回傳的來的,還是onRejected回傳來的。當一直有回傳值時就可以一直傳遞迴傳值,當出現錯誤時,就會因為獲取不到之前的值,會導致之前的值消失不見。

then方法中的兩個函式傳入參數,第1個onFulfilled函式是在promise物件有值的情況下才會執行,也就是進入到fulfilled(已實現)狀態。第2個onRejected函式則是在promise物件發生錯誤、失敗,才會執行。這兩個函式都可以寫出來,但為了方便進行多個不同程式碼的連鎖,通常在只使用then方法時,都只寫第1個函式傳入參數。

而錯誤處理通常交給另一個catch方法來作,catch只需要一個函式傳入參數,(出自MDN):

p.catch(onRejected);

p.catch(function(reason) {
   // rejection
});

catch方法相當於then(undefined, onRejected),也就是then方法的第一個函式傳入參數沒有給定值的情況,它算是個then方法的語法糖。catch方法正如其名,它就是要取代同步try...catch語句用的異步例外處理方式。

註: 不過也因為catch方法與try...catch中的catch同名,造成IE8以下的瀏覽器產生名稱上的衝突與錯誤。有些函式庫會用caught這個名稱來取代它,或是乾脆用then方法就好。

對於值的傳遞情況,需要注意是在中途發生有promise物件有rejected(已拒絕)的情況。在下面的範例中,第二個then方法假設在中途發生錯誤,這樣會導致下一個執行被強迫只能使用catch方法,catch方法中的回調(相當於then中的onRejected函式)是只能得到理由(錯誤)而得不到值的,所以導致最後一個then方法中的onFulfilled函式獲取不到之前的值。這一段內容如果你並不是很理解,你可以先看其他章節的內容,再回來這裡重新執行一次這個範例。

const p1 = new Promise((resolve, reject) => {
    resolve(4)
})

p1.then((val) => {
        console.log(val) //4
        return val + 2
    })
    .then((val) => {
        console.log(val) //6
        throw new Error('error!')
    })
    .catch((err) => { //catch無法抓到上個promise的回傳值
        console.log(err.message)
        //這裡如果有回傳值,下一個then可以抓得到
        //return 100
    })
    .then((val) => console.log(val, 'done')) //val是undefined,回傳值消息

JavaScript ES6 Promise Object 物件

Promise 是一種非同步 (asynchronous) 編程的解決方案,所謂的 Promise,簡單來說它是一個等待非同步操作完成的物件,當事件完成時,Promise 根據操作結果是成功、或者失敗,做相對應的處理動作。

一個 Promise 物件 (只) 會處於下面三種狀態之一:

  1. pending - 初始狀態 (進行中)
  2. fulfilled - 事件已完成
  3. rejected - 事件已失敗

Promise 狀態的改變只有兩種可能:

  1. 從 pending 變成 fulfilled
  2. 從 pending 變成 rejected

而一但狀態改變就會固定,永遠不會再改變狀態了。

非同步最常見的例子像是AJAX,傳統在處理非同步事件時會用一堆 nested callbacks,Promise 則是提供另外一種解決方案,讓你更直觀地控制非同步操作。

來看看建立一個 Promise 物件的語法:

var promise = new Promise(function(resolve, reject) {

  // ...

  if (異步操作成功) {
    resolve(value);
  } else {
    reject(error);
  }

});

Promise 構造函數 (constructor) 接受一個函數作為參數,這個函數會在建立 Promise 物件的同時立刻被執行,該函數有兩個參數分別是 resolve 函數和 reject 函數,resolve/reject 這兩個函數會由 JavaScript interpreter 自動傳入。

  • resolve(value) 函數的用途是用來將 Promise 物件的狀態變為 fulfilled (已完成),在非同步操作成功時調用,你可以將非同步操作的結果當作參數一起傳入
  • reject(error) 函數的用途是用來將 Promise 物件的狀態變為 rejected (已失敗),在非同步操作失敗時調用,你可以將非同步操作的錯誤當作參數一起傳入

此外,當 Promise constructor 的函數參數執行時,如果內部發生錯誤 throw error,Promise 物件的狀態會自動變成 rejected。

Promise.prototype.then(onFulfilled, onRejected)

Promise 物件生成後,可以用 then() 方法來綁定當 fulfilled 或 rejected 狀態時,分別要執行的函數。

promise.then(function(value) {
  // 當狀態是 fulfilled (成功) 時,執行這個函數
  // value 是透過 resolve() 傳進來的參數

}, function(error) {
  // 當狀態是 rejected (失敗) 時,執行這個函數
  // error 是透過 reject() 傳進來的參數

});

then() 方法接受兩個函數作為參數:

  • 第一個函數是當 Promise 狀態變為成功時會被調用
  • 第二個函數是當 Promise 狀態變為失敗時會被調用,這個參數是選擇性的不一定需要

舉一個 Promise 實際使用的例子:

let promise = new Promise(function(resolve, reject) {

  // 執行非同步的 setTimeout
  setTimeout(function(){
    // 250ms 過後,將 Promise 物件狀態改為成功
    resolve('Success!');
  }, 250);

});

promise.then(function(successMessage) {
  // 當 Promise 物件狀態變成功後執行這個函數

  console.log('Yay! ' + successMessage);
});

// 250ms 後你會看到輸出 "Yay! Success!"

Chaining 串接

then() 和 catch() 方法執行後都會返回一個新的 Promise 物件,讓你可以使用 chaning 的語法。

而後面的 then() 會接收前一個 then() 的 return value 當作參數。

如果 return value 的型態不是 Promise,會先執行Promise.resolve()

例如:

var hello = new Promise(function(resolve, reject) {
  resolve('Hello');
});

hello.then(function(str) {
  return str + ' World';

}).then(function(str) {
  return str;

}).then(function(str) {
  console.log(str);
});

// 最後輸出 "Hello World"

Promise.all(iterable)

Promise.all() 函數用來將多個 Promise 物件包裝成一個 Promise 物件,他接受的參數可以是一個陣列,陣列中放不同的 Promise 物件。

如果陣列中有元素的型態不是 Promise,會先執行Promise.resolve()

而新的 Promise 物件的狀態會怎麼改變?

  1. 狀態變為 fulfilled: 如果它包含的所有 Promise 物件狀態都變為 fulfilled。而所有 Promise 物件個別的返回值,會被組成一個陣列傳進 all Promise 物件的 callback
  2. 狀態變為 rejected: 如果它包含的其中一個 Promise 物件狀態變為 rejected。而第一個被 reject 的值會被傳進回 all Promise 物件的 callback
var p1 = Promise.resolve(3);

var p2 = 1337;

var p3 = new Promise(function(resolve, reject) {
  setTimeout(resolve, 100, 'foo');
}); 

Promise.all([p1, p2, p3]).then(function(values) { 
  console.log(values);
});

// 會顯示 [3, 1337, "foo"]

Promise.race(iterable)

Promise.race() 函數和 Promise.all() 一樣用來將多個 Promise 物件包裝成一個 Promise 物件。

不同的地方在於,只要它包含的所有 Promise 物件其中任何一個的狀態先改變,race Promise 物件的狀態就會跟著改變,率先改變狀態的 Promise 物件參數會直接傳給 race Promise 物件的 callback。

 var p1 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 500, 'one'); 
});
var p2 = new Promise(function(resolve, reject) { 
    setTimeout(resolve, 100, 'two'); 
});

Promise.race([p1, p2]).then(function(value) {
  console.log(value);
});

// 會顯示 "two",因為 p2 比較快被 resolve

Promise.resolve(value)

Promise.resolve() 函數用來將一個物件轉型為 Promise (如果它不是一個 Promise 物件),然後立刻 resolve 它。

Promise.resolve('Success').then(function(value) {
  console.log(value);
}, function(value) {
  console.log('Fail');
});

// 輸出 "Success"

Promise.reject(reason)

Promise.reject() 函數用來將一個物件轉型為 Promise (如果它不是一個 Promise 物件),然後立刻 reject 它。

Promise.reject(new Error('Fail')).then(function(error) {
  console.log('Success');
}, function(error) {
  console.log('Fail');
});

// 輸出 "Fail"

參考連結:

https://www.fooish.com/javascript/es6/Promise.html

http://liubin.org/promises-book/\#how-to-write-promise

https://eyesofkids.gitbooks.io/javascript-start-es6-promise/content/contents/flow\_n\_error.html

https://itw01.com/26POEKN.html

https://benjaminlu.github.io/blog/2017/08/22/Javascript%E9%9D%9E%E5%90%8C%E6%AD%A5%E8%99%95%E7%90%86-Promise-Async-Await/

https://www.jianshu.com/p/d4d437d4e83f

https://medium.com/frochu/%E5%88%A5%E9%80%99%E6%A8%A3%E4%BD%BF%E7%94%A8promise-d4f5a731adb4

http://cythilya.blogspot.tw/2016/08/node-promise-q.html

http://huli.logdown.com/posts/292655-javascript-promise-generator-async-es6

https://noootown.wordpress.com/2016/11/13/callback-promise-fetch-yield-async-await/

http://eddychang.me/blog/javascript/88-promise-basic-usage.html

https://eyesofkids.gitbooks.io/javascript-start-es6-promise/content/contents/basic\_usage.html

https://andyyou.github.io/2017/06/27/js-promise/

http://www.oxxostudio.tw/articles/201706/javascript-promise-settimeout.html

results matching ""

    No results matching ""