同步非同步
一般來說 JavaScript 裡面有分成同步 sync 和非同步 async,在同步模式下,每個任務必須按照順序執行,後面的任務必須等待前面的任務執行完成,非同步模式則相反,後面的任務不用等前面的,各自執行各自的任務,例如setTimeout
、setInterval
都是這種模式。
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 有三種狀態:
pending
未發生、等待的狀態。到下週前,你還不知道這件事會怎樣。resolved
完成/履行承諾。你媽真的買了手機給你。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)
}
})
上面這段程式碼應該已經充分解釋概略的觀念。
- 第一行我們使用一個 Boolean
isMomHappy
定義媽媽是否開心。 - 我們宣告一個 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 物件 (只) 會處於下面三種狀態之一:
- pending - 初始狀態 (進行中)
- fulfilled - 事件已完成
- rejected - 事件已失敗
Promise 狀態的改變只有兩種可能:
- 從 pending 變成 fulfilled
- 從 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 物件的狀態會怎麼改變?
- 狀態變為 fulfilled: 如果它包含的所有 Promise 物件狀態都變為 fulfilled。而所有 Promise 物件個別的返回值,會被組成一個陣列傳進 all Promise 物件的 callback
- 狀態變為 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://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