0%

JS核心篇-Promise

為什麼需要Promise?

我們需要Promise的原因是需要解決以下幾種問題:
1. 回呼地獄
當我們需要安排很多個非同步函式的操作順序時,必須在一個函式執行結束之後,
再呼叫其他的非同步函式繼續執行下去,這樣會導致程式碼很亂,而且不容易管理。
2. 寫法不一致
在許多的非同步套件的寫法都不太一樣,這樣的話就會導致程式碼的整體寫法不夠一致。
3. 無法同時執行
在JS的程式碼中,我們無法保證每一個非同步函式都是同時執行。
另外,有時候,我們也希望當所有指定的非同步函式都執行完畢之後,在執行某些內容。
如果,沒有適當的安排的話,這樣的效果是非常難完成的。

那以上就是沒有Promise出現之前,常常遇見的問題囉。

以下先舉個解決回呼地獄的範例
假設我們有在專案檔裡面安裝了axios這個Promise套件

1
2
3
4
5
6
7
8
9
const api = 'https://randomuser.me/api/'
axios.get(api)
.then((res) => {
console.log(1, res)
return axios.get(`${api}?seed=${res.data.info.seed}`)
})
.then((res) => {
console.log(2, res)
})

可以看到以上這個範例在axios套件裡面的Promise就有固定的寫法,利用get來執行ajax的行為,利用then來安排非同步函式的執行順序,
這樣的寫法就解決了寫法不一致 和 回呼地獄的問題。

另外,再舉個解決無法同時執行非同步的範例

1
2
3
4
5
const api = 'https://randomuser.me/api/'
Promise.all([axios.get(api), axios.get(api)])
.then(([res1, res2]) => {
console.log(res1, res2)
})

我們利用了Promise.all的方式,來確保同時執行兩個非同步的函式,並在這兩個函式執行完畢後,才接著執行then後面接的內容。

Promise 基礎概念

當我們創建了一個Promise物件,並執行它,此時,會處於pending狀態,當執行結束之後,會有兩種結果,
一個是執行成功,這個時候會進到then裡面的結果,並回傳Promise物件中的resolve內容。
另一個是執行失敗,這個時候會進到catch裡面,並回傳Promise物件中的reject的內容。

首先,先上個簡單的範例

1
2
3
4
5
6
const a = new Promise((resolve, reject) => {
resolve('success')
})
a.then((res) => {
console.log(res)
})

你可以看到,我們利用a來建立一個Promise物件,那這邊要特別注意的是,建立Promise物件時,必須要傳入一個callback function,才能順利建立喔,然後,這個callback function傳入的參數分別是resolvereject
第二,你可以看到我們在建構Promise物件中有一個resolve內容,那這個內容就會在我們調用a物件時,並呼叫then,就會回傳resolve的結果。
若今天,你在建構Promise物件內容中,也有加入reject的內容的話,就會在調用Promise物件並呼叫它catch時,就會回傳reject的結果。

但是,我們一般都不會這樣創建與使用Promise物件的,因為,上面這個範例a是一個物件,無法傳任何參數進去,使用上比較沒有彈性。
所以,通常我們都會用function回傳一個Promise物件,這樣使用上會比較有彈性。

這邊,就使用函式來創建一個Promise物件並使用它

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function PromiseFn(num) {
return new Promise((resolve, reject) => {
setTimeout(()=>{
if(num) {
resolve('成功')
} else {
reject('失敗')
}
}, 10)
})
}

PromiseFn(0)
.then(res => {
console.log(res)
})
.catch(res => {
console.log(res)
})

console.log('程式結束')

上面這個範例,就是利用function回傳一個新的Promise物件,並對這個函式傳入一個參數。
那在這邊我們另外想要特別的呈現js的同步和非同步之間的執行次序的差別,setTimeout是一個非同步函式,那console.log是一個同步函式,
所以,以上的範例的執行結果,會先執行’程式結束’之後,最後,才會執行在Event Queue裡面的setTimeout這個非同步的內容,所以,接著才會呈現Promise最終執行的結果。

鏈接技巧

當我們希望安排非同步函式們的執行順序時,我們就需要用到Promise chain的技巧來安排非同步函式之間的執行順序。
先來個例子

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
function PromiseFn(num) {
return new Promise((resolve, reject) => {
setTimeout(()=>{
if(num) {
resolve(`成功${num}`)
} else {
reject(`失敗${num}`)
}
}, 10)
})
}

PromiseFn(10)
.then(res => {
console.log(res)
return PromiseFn(1)
})
.then(res => {
console.log(res)
return PromiseFn(0)
})
.then(res => {
console.log(res)
return PromiseFn(2)
})
.catch(res => {
console.log(res)
return PromiseFn(100) // 執行失敗之後,接著要執行的函式
})
.then((res) => {
console.log(res)
})

上面這個範例說明了,當我們想要安排非同步函式執行順序,必須在a函式執行完畢的內容中,加上一個return 後面接的就是接著要執行的非同步函式。
如此,就可以將接續執行的非同步函式的內容再傳遞到下一個Promise中。
如果,中間只要執行有失敗就會直接跳到執行catch的內容,若catch的內容裡面也有return 接著要執行的非同步函式,它就會接著再執行後面的內容。

then可以執行resolve和reject

之前雖然有說then是執行resolve的結果,catch是執行reject的內容。但其實then都可以執行resolve和reject的結果喔。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PromiseFn(10)
.then((res)=>{
console.log(res)
return PromiseFn(100)
},
(rej)=>{
console.log(rej)
return PromiseFn(100) // 執行失敗之後,接著要執行的函式
})
.then((res)=>{
console.log(res)
return PromiseFn(100)
},
(rej)=>{
console.log(rej)
return PromiseFn(100) // 執行失敗之後,接著要執行的函式
})

上面的範例你可以看到在then裡面,就同時有放resolve和reject要執行的結果。

Promise 常用方法

這邊介紹了常用的兩個Promise的api分別是Promise.allPromise.race

Promise.all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function PromiseFn(num, time = 500) {
return new Promise((resolve, reject) => {
setTimeout(()=>{
if(num) {
resolve(`成功${num}`)
} else {
reject(`失敗${num}`)
}
}, time)
})
}

Promise.all(
[
PromiseFn(1, 2000),
PromiseFn(2, 300),
PromiseFn(3, 100),
])
.then((res) => {
console.log(res)
})
.catch((rej) => {
console.log(rej)
})

上面這個範例可以看到,我們傳入三個非同步函式到Promise.all裡面,當傳入的三個非同步函式皆執行成功,就會進到then的內容,那這個res會回傳一個陣列,這個陣列存的就是Promise.all裡面的函式回傳的resolve的結果。
Promise.all裡面只要一個函式執行失敗,就會直接進到catch的執行程序中。

Promise.race

這個Promise的api的功能,傳入的函式們,只要有任一個函式第一個執行完,就會直接回傳它的執行結果,所以,如果這個第一個執行完的函式是執行成功,
就會進到then的執行程序,如果,如果這個第一個執行完的函式是執行成功失敗,就會進到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
function PromiseFn(num, time = 500) {
return new Promise((resolve, reject) => {
setTimeout(()=>{
if(num) {
resolve(`成功${num}`)
} else {
reject(`失敗${num}`)
}
}, time)
})
}

Promise.race(
[
PromiseFn(0, 50),
PromiseFn(2, 300),
PromiseFn(3, 100),
])
.then((res) => {
console.log(res)
})
.catch((rej) => {
console.log(rej)
})

上面這個範例,第一個執行完的會是PromiseFn(0, 50),而因為它是執行失敗會回傳reject的內容,所以,接著會執行catch的內容。

Promise 與 Ajax

通常Promise在專案裡面我們都會搭配Ajax跟遠端請求資料的程式內容一起搭配使用。

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
const url = 'https://jsonplaceholder.typicode.com/todos/1'
function get(api) {
return new Promise((resolve, reject) => {
let req = new XMLHttpRequest()
req.open('GET', api)
req.onload = function() {
if(req.status === 200) {
resolve(req.response)
} else {
reject(req)
}
}
req.responseType = 'json';
req.send()
})
}

get(url)
.then((res)=> {
console.log('Promise', res)
return get(url)
})
.then((res)=>{
console.log('Promise1', res)
})
.catch((rej) => {
console.log(rej)
})

上面這個範例,我們創了一個回傳新Promise物件的函式,並且在它的Promise內容中,是跟遠端索取資料。
最後,我們再搭配Promise來執行這個非同步功能的索取遠端資料的執行程序。