uncategorized

Promise

JavaScript 有些程式是非同步的(例如 ajax, setTimeout, setInterval),為了確保這些非同步程式能照我們想要的順序執行,我們使用了回呼函式 (以下稱 callback function) 來達成我們的需求,但 callback function 有個問題是只要程序一多,程式碼會變得非常難以閱讀,於是 ES6 提出了一個解決方案:Promise。

甚麼是 Promise?

首先要知道,Promise 是一個物件(object),它代表一個即將完成、或失敗的非同步操作,以及它所產生的值。一個 Promise 物件透過 new 及其建構式建立。這個建構式接收一個叫作”執行器函式(executor function)”的引數。此函式裡面有兩個 callback function 作為引數,分別是 resolvereject

1
new Promise( /* executor */ function(resolve, reject) { ... } );

resolve 與 reject 函數的作用

任何一個 Promise 物件在剛被建立時,一定都是處於 pending 狀態,只有執行完裡面的非同步程式後,才能確認該 Promise 是成功或失敗。

如果該 Promise 的非同步程式成功完成時,將會執行第一個函式 resolve 之結果值,相對的,若非同步程式失敗時,Promise 將會執行第二個函式 reject 之結果值。簡單的說,即是把這兩個 callback function 當作該 Promise 成功或失敗的接口。

成功便調用第一個回呼函數 resolve,將參數傳遞出去。
失敗便調用第二個回呼函數 reject,將參數傳遞出去。

1
2
3
4
5
const myPromise = new Promise(function(resolve, reject){
// do something
resolve(value) // 成功便執行 resolve 的 value 值
reject(reason) // 失敗便執行 reject 的 reason 值
})

resolve 與 reject 可任意命名

Executor 裡面的 callback function(resolve 與 reject)可用其他名稱代替。

1
2
3
4
5
const myPromise = new Promise(function(successed, failed){
// do something
successed(value) // 成功便執行 successed 的 value 值
failed(reason) // 失敗便執行 failed 的 reason 值
})

第二個 reject 函式不一定要放

resolve 或 reject 函式不必兩個都放,如果你確定你的 Promise 裡面的程式一定會執行成功,可以只放第一個 callback function,也就是 resolve。如果你的 Promise 可能有失敗結果,就不能偷懶,一定要放兩個 callback function,第一個 callback function 是成功時執行,第二個 callback function 是失敗時執行。

1
2
3
4
const myPromise = new Promise(function(reject){
// do something
reject(value) // 成功便執行 reject 的 value 值
})

上面有個小小的陷阱,在 executor 裡面我只放了一個叫 reject 的 callback function,雖然它叫 reject,但它其實是 Promise 成功時才會執行的 callback function,因為這裡綜合了以上所提到的兩個特性:

  • resolve 與 reject 可任意命名
  • 第二個 reject 函式可放可不放

因為 executor 裡面只有一個 callback functoin,所以它其實代表的是 Promise 成功時才會執行的接口,也就是一般所認知的 resolve 函式。

then()

then()方法回傳一個 Promise 物件。它接收兩個引數: Promise 在成功及失敗情況時的 callback function。

then()第一個 callback function 是 Promise 對象的狀態變為 resolved 時調用,第二個 callback function 是 Promise 對象的狀態變為 rejected 時調用,其中第二個 callback function 是可選的,不一定要提供。這兩個函數都接受 Promise 對象傳出的值作為參數。

1
2
3
4
5
6
7
8
9
10
11
const myPromise = new Promise(function(resolve, reject){
// do something
resolve(value)
reject(reason)
})

myPromise.then(function(value){
// myPromise 狀態為成功時,執行此處程式碼
}, function(reason){
// myPromise 狀態為失敗時,執行此處程式碼
})

then() 串連

還記得我們使用 Promise 的初衷嗎?

為了讓非同步程式以同步程式操作的流程進行,我們使用 then 方法,依序呼叫兩個以上的非同步函數,而每個 then 方法回傳一個 Promise 以進行方法串接(method chaining),我們稱之為建立 Promise 鏈。

1
2
3
4
5
myPromise.then(function(json) {
return json.post;
}).then(function(post) {
// ...
})

catch()

catch 與 then 用法相反,它接收 Promise 失敗時的 reject 函式,一般放在 Promise 鏈的最後。

1
2
3
4
5
6
7
8
9
10
11
const myPromise = new Promise(function(resolve, reject){
// do something
resolve(value)
reject(reason)
})

myPromise.then(function(value){
// myPromise 狀態為成功時,執行此處程式碼
}.catch(function(){
// myPromise 狀態為失敗時,執行此處程式碼
}))

用 catch 定義 reject,別用 then 的第二個 callback function

一般來說,盡量不要在 then 方法裡面定義 Reject 狀態的 callback function(即 then 的第二個參數),建議使用 catch 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// bad
promise
.then(function(data) {
// success
}, function(err) {
// error
});

// good
promise
.then(function(data) { //cb
// success
})
.catch(function(err) {
// error
});

實作

接下來讓我們來實作一個簡單的 promise 範例。

1
2
3
4
5
6
7
8
9
10
11
function logWord(word){
setTimeout(function(){
console.log(word)
}, Math.floor(Math.random() * 1000))
}
function allWord(){
logWord('a')
logWord('b')
logWord('c')
}
allWord();

function logWord 是一個 setTimeout 程式,會 不定時 console 出我們自定的參數,當我們用一個函式 allWord() 把數個 logWord() 包起來,執行時會發現裡面的 logWord 是隨機觸發的,原因是因為Math.floor(Math.random() * 1000)

但你想要 allWord() 裡面的 logWord() 是依序執行出 a b c,而不是隨機 b c a 或 c a b 時,我們會怎麼做?以前的做法是使用 callback 函式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function logWord(word, cb){  //插入 callback
setTimeout(function(){
console.log(word)
cb(); //在主程式最後一行執行
}, Math.floor(Math.random() * 1000))
}

function allWord(){
logWord('a', function(){ //放入第1個 callback
logWord('b', function(){ //放入第2個 callback
logWord('c', function(){}) //第3個 callback 裡面是空的,因為已沒有其他需要做的事
})
})
}
allWord();

在 logWord() 插入一個 callback 參數(這裡取名為 cb) ,然後在 setTimeout 主程式的最後一行執行它。

接著執行以下動作:

  1. 在 allWord() 裡,將第一個執行的 logWord(‘a’) 插入 callback 當作第二個參數
  2. 把 logWord(‘b’) 放在 logWord(‘a’)的 callback,確保會在 console 出 a 才會執行 logWord(‘b’)
  3. 把 logWord(‘c’) 放在 logWord(‘b’)的 callback,確保會在 console 出 a 才會執行 logWord(‘c’)
  4. logWord(‘c’) callback fuction 為空值,因為此時沒其他事要做了
  5. 執行 allWord(),印出 a b c

這時你的程式就會依序印出 a b c 了。

但看看上面的程式碼,我們光只是寫三層 callback 就很累了,更別說之後萬一需要寫更多層的時候了,這時就會陷入萬劫不復的 callback hell (回呼地獄)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
function callback(hell){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hell('no', function(){
hollyshit()
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
})
}

我們用 Promise 來改寫一下上面的程式。

首先在原本的 logWord 返回一個新建的 Promise,語法為本文最一開始的 new Promise( function(resolve, reject) { … } )

然後把 setTimeout 主程式放入 Promise 裡。為了讓我們的 logWord 可以串連,我們在 setTimeout 主程式最後一行放入 resolve(),讓這個 Promise 物件成功執行時,馬上拋出 resolve。

1
2
3
4
5
6
7
8
function logWord(word){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log(word)
resolve()
}, Math.floor(Math.random() * 1000))
})
}

接著我們在 allWord() 裡,用 then 把成功執行的 logWord() 串連起來

1
2
3
4
5
6
7
8
9
10
function allWord(){
logWord('a')
.then(function(){
return logWord('b')
})
.then(function(){
return logWord('c')
})
}
allWord();

為了簡潔美觀,我把它改成箭頭函式的寫法。

1
2
3
4
5
6
function allWord(){
logWord('a')
.then(() => logWord('b'))
.then(() => logWord('c'))
}
allWord();

最後的成果,這樣就可依序印出 a b c

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function logWord(word){
return new Promise(function(resolve, reject){
setTimeout(function(){
console.log(word)
resolve()
}, Math.floor(Math.random() * 1000))
})
}
function allWord(){
logWord('a')
.then(() => logWord('b'))
.then(() => logWord('c'))
}
allWord();

總結

使用 Promise 讓我們可以把非同步操作以同步操作的流程表達出來,它把執行程式和處理結果的程式清晰地分離,同時簡潔的語法,也解決了以前多層 callback function 不好維護的問題。

對於 Promise 我其實還有很多未整理出來的地方,但希望透過這篇文章讓我對我目前所了解的 Promise 做個梳理,有興趣的朋友也可以參考下面所附連結進而更了解 Promise。

參考連結:
React 16 - The Complete Guide
你所不知道的JS:ES6與未來發展
javascript callback functions tutorial
Promises - Part 8 of Functional Programming in JavaScript
[JavaScript 教學] Callback與Promise (非同步編程基礎)
JavaScript Promise 漂亮的串接非同步事件
MDN - Promise
從Promise開始的JavaScript異步生活
Javascript的非同步之旅
JavaScript Promise:簡介
JAVASCRIPT.INFO - Promise

Share