0%

一次搞懂Promise

第一次看到promise的實戰, 是出現在口罩地圖的示範碼, 當時老師透過promise載入兩個ajax. 自己之前也有寫了一篇文章在講相關的概念, 但是覺得還是沒有掌握好.

知道什麼時候要用是一回事, 但是怎麼用又是一回事, 還有就是為什麼要用? 所以寫了這篇文章, 希望能徹底了解Promise, async, await.

(此篇文章是以筆者的理解加以撰寫,所以基礎觀念很多都跳過, 不適合基礎者閱讀)

JS的運作原理

JS本身是單執行緒, 也就是一次只能處理一件事的語言, 但程式要處理的事情很多, 單執行緒不太夠用啊, 怎麼辦?

那就要給優先順序

JS機制是會把非同步事件移動到最後面執行
哪些是非同步事件呢?

名詞解釋

簡單了解了JS運作原理, 不免俗的先來個名詞解釋.

Ajax

是一個技術名稱 ⇒ 取得遠端資料的非同步行為

為什麼是非同步?
問這問題先假設如果是同步, 而遠端的資料又多又雜,又要先處理的話, 那整個程式都卡在那

以最近最夯的買酒精為例:

原本要結帳的沒辦法結帳, 如果先等排隊買酒精的人都結帳完, 才處理後面的結帳, 那這樣是一件沒有效率的事情.

那怎麼辦?

先處理結帳的客戶, 買酒精的人繼續排隊. 等到後面結帳的人都沒人了, 再來處理酒精的結帳.

Promise

是一個語法 ⇒ 處理非同步行為
為什麼要用 ⇒ 因為要有一個標準化的介面
白話一點: 透過Promise管理ajax行為, Promise是用來改善 JavaScript 非同步的語法。

再白話一點: 我要怎麼管理排隊的人潮拉?

Promise 與 Async、Await 有什麼關係?

Async、Await 可以基於 Promise 讓非同步的語法的結構類似於 “同步語言”,更易讀且好管理。

白話一點: 優化promise的孿生兄弟
再白話一點: 比原本Promise方法更好的作法

Promise 的結構及狀態

原生調用

那如果console.dir(Promise) 會出現什麼呢?

Promise 本身是一個建構函式,函式也是屬於物件的一種,因此可以附加其它屬性方法在上,透過 console 的結果可以看到 Promise 可以直接使用 all、race、resolve、reject 的方法

  • Promise.all
  • Promise.race
  • Promise.resolve
  • Promise.reject

(若想了解用法, 可以直接參考段落Promise方法)


Promise 建構函式的同時,必須傳入一組函式作為參數,此函式的參數包含resolve, reject,這兩個方法分別代表成功與失敗的回傳結果,特別注意這兩個僅能回傳其中之一,且必定只能回傳一次回傳後表示此 Promise 事件結束

建構函式 new 調用

先來看兩種調用的方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 在函數裡面的寫法
const promise2 = () => {
return new Promise((resolve, reject) => {

})
}

console.dir(promise2())

// 一般調用
const p = new Promise((resolve, reject)=> {
})

console.dir(p)

那如果console.dir(p) 會出現什麼呢?

Promise 建構函式 new 出的物件,則可以使用其中的原型方法, (在 prototype 內,其中就包含 then、catch、finally,這些方法則必須在新產生的物件下才能呼叫。

  • p.then() => Promise 回傳正確
  • p.catch() => Promise 回傳失敗
  • p.finally() => 非同步執行完畢

狀態

在 Promise 的執行函式中,可以看到以下兩個屬性:

  • [[PromiseStatus]]: "pending" -> 表示目前的進度狀態
  • [[PromiseValue]]: undefined -> 表示 resolvereject 回傳的值

Promise 中會使用 resolve 或 reject 回傳結果,並在調用時使用 then 或 catch 取得值。

更改狀態看看

1
2
3
4
5
6
7
const promise2 = () => {
return new Promise((resolve, reject) => {
resolve('失敗');
})
}

console.dir(promise2())

then vs catch 舉例

上列的三種狀態每次執行必定會經過 Pending,接下來進入 Fulfilled 或 Rejected 的其中之一,並且可以使用 then() 及 catch() 取得成功或失敗的結果。

1
2
3
4
5
6
7
8
9
10
let a = new Promise(function (resolve, reject) {
// 這一步只是改變PromiseValue, 但還不能調用
resolve(3)
})

a.then(function (result) {
console.log(result); //3
}).catch(function(result){
console.log('失敗')
})

Promise 另一個特點在於 then、catch 都可以使用鏈接的方式不斷的進行下一個任務.

實例來一下

筆者這邊有找到一個很好的範例來解釋

  • 為什麼要用?
  • 怎麼用? 知道有這個武器是理所當然, 但是什麼時候要把這武器叫出來?

首先我要可以印出A ⇒ B ⇒ C的順序

無法確定callback回傳的順序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function logWord(word){
setTimeout(function() {
console.log(word)
}, Math.floor(Math.random() * 100) + 1
// return value between 1 ~ 100
)
}

function logAll(){
logWord("A")
logWord("B")
logWord("C")
}
logAll()

改成callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function logWord(word, callback) {
setTimeout(function() {
console.log(word)
callback()
}), Math.floor(Math.random() * 100) + 1
}

function logAll(){
logWord("A", function() {
logWord("B", function() {
logWord("C", function() {})
})
})
}
// Callback Hell
logAll()

改成Promise

簡單來說就是在非同步事件上在外面包裝一層

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function logWord(word){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
console.log(word)
}, Math.random()*100 + 1);
})
}

function logAll(){
logWord('C')
// 這是省略return的寫法
.then(success => logWord('B'))
.then(success => logWord('A'))
}


logAll() // 有順序性的的處理 C => B => A

改成Async & Await 寫法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function logWord(word){
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve()
console.log(word)
}, Math.random()*100 + 1);
})
}

// async 和 await 就是把原本的then給取代, 讓原本是非同步的事件處理起來像是同步一樣, 程式碼一行一行往下看
async function logAll(){
await logWord('C')
await logWord('B')
await logWord('A')
}

logAll() // 有順序性的的處理 C => B => A

Promise 方法

展開後可以看到以下方法:

  • all -> 多個 Promise 行為同時執行,全部完成後統一回傳。
  • race -> 多個 Promise 同時執行,但僅回傳第一個完成的。
  • Promise.reject, Promise.resolve -> 定義 Fulfilled 或 Rejected 的 Promise 物件。

這邊先定義一個函式, 為後續例子所用

1
2
3
4
5
6
7
function promise(num, time=500){
return new Promise((resolve, reject) => {
setTimeout(() => {
num ? resolve(`${num}`,'成功'):reject('失敗')
}, time);
})
}

Promise.all

鳴槍起跑, 全部一起跑, 比賽什麼時候結束? 全部的馬跑完才會結束

透過陣列的形式傳入多個 promise 函式,在全部執行完成後回傳陣列結果

這個方法很適合用在多支 API 要一起執行,並確保全部完成後才進行其他工作時。

1
2
3
// 這是針對上一part的 resolve, reject的函式所編寫
Promise.all([promise(1), promise(2), promise(3, 3000)])
.then(success => console.log(success))

Promise.race

透過陣列的形式傳入多個 promise 函式,在全部執行完成後回傳單一結果,結果為第一個運行完成的,以下範例來說就會回傳 promise(1) 的結果

1
2
Promise.all([promise(1), promise(2), promise(3, 3000)])
.then(success => console.log(success))

適合用在完整性, 而不是順序性, 小案子適合

Promise.reject, Promise.resolve

這兩個方法是直接定義 Promise 物件已經完成的狀態(resolve, reject),與 new Promise 一樣會產生一個新的 Promise 物件,但其結果是已經確定的,以下提供範例說明:

1
2
3
4
5
6
7
// 使用 Promise.resolve 產生一個新的 Promise 物件,此物件可以使用 then 取得 resolve 的結果。
var result = Promise.resolve('result');
result.then(res => {
console.log('resolved', res); // 成功部分可以正確接收結果
}, res => {
console.log('rejected', res); // 失敗部分不會取得結果
});

簡單來說, 如果直接console.dir(result), 會得到如下的圖片

在[[PromiseStatus]]裡面, 沒有上述所說的三種狀態, 且連值也都被定義好了

Await and Async

await: 等待
async: 非同步

Async

async 本身也是類似 Promise,在正確執行的情況下 return 會傳回 resolved 的狀態,也可以使用 then 來接收正確的資料。

底下例子可以窺知一二

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 一般來說Promise的寫法
function run2(){
return new Promise((resolve, reject) => {
resolve('hello')
})
}
console.log(run2())

// 改成async以後
async function run(){
return 'hello'
}

console.log(run())

run()run2(), console出來, 結果一樣, 所以async根本就是Promise的語法糖

可以看出, return回來的東西就是resolveValue

但其實這樣還不夠甜, 甜是甜在await

Await

Await 顧名思義就是等待,在這個 Promise 結束前後面的程式碼都無法被執行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function run(){
console.log('hello')
}

function walk(){
console.log('moto')
}

(async ()=>{
// await是幹嘛用的? 他可以取代掉then和catch, 先等run執行完後, 賦予到a, 接下來執行walk
// 其實就把它想成怎麼執行function的順序這樣
let a = await run()
let b = await walk()
let c = await run()
})()

錯誤

出現錯誤了怎麼辦? 用try, catch去接

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

// 1. 等待run這個非同步的事件, 完成後把結果寫在a裡面
// 2. 如果非同步回來是錯的怎麼辦? 告訴電腦幫我試試看啊
// 3. 如果錯誤不做try, catch, 程式碼會因此中斷下來
// 4. 如果成功就寫進去, 如果失敗程式碼就會停下來

async function run(){
return 'hello'
}

async function failed() {
throw 'error'
}


(async () => {
let a = await run()
try{
let b = await failed()
}catch(e){
console.log(e)
}
let c = await failed()
})()


// 優化寫法
async function run2() {
let result
try {
result = true
} catch (e) {
result = false
} finally{
console.log('finally')
return 'xxx'
}
}

console.log(run2())

補充

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
48
49
50
51
52
53
54
55
56
57
58
59
function promiseFn(num){
return new Promise((resolve, reject) => {
setTimeout(() => {
if(num){
resolve(`${num} :成功`)
}else{
resolve(`${num} :失敗`)
}
}, 0);
})
}

//成功用then接收, 失敗用catch接收, 一旦失敗還是可以繼續執行, 只要在catch裡面再加上回傳值
promiseFn(1)
.then(res => {
console.log(res)
return promiseFn(0)
})
.then(res => {
console.log(res)
return promiseFn(2)
})
.then(res => {
console.log(res)
})
.catch(res => {
console.log(res)
return promiseFn(4)
})
.then(res => {
console.log(res)
})


// 答案會是 1 成功 -> 失敗 -> 4 成功

// 不管什麼catch了, 都是用then接收, 注意後面是接收兩個cb
promiseFn(0).then((res)=>{
console.log(res)
}, (rej)=>{
console.log(rej)
})

promiseFn(0)
.then((res) => {
console.log(res)
return promiseFn(3)
}, (rej) => {
console.log(rej)
return promiseFn(4)
})

.then((res) => {
console.log(res)
}, (rej) => {
console.log(rej)
})

// 答案會是 失敗 -> 4 成功

Fetch建立多個Ajax連續處理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
fetch('https://reqres.in/api/unknown/2')
.then(res => {
return res.json()
})
.then(response => {
console.log(response)
// 關鍵在於這一步, 成功以後再回傳一個新的url
return fetch('https://reqres.in/api/users/2')
})
.then(res => {
return res.json()
})
.then(response => {
console.log(response)
})

參考文獻