從 Callback 到 Promise 再到 Generator

很有意思的寫法。

簡述

這篇是繼 快速入門 generator 的延伸文章,主要是想介紹一下用 generator 來實現非同步操作是怎麼樣的感覺。

範例

這邊我們先從 callback 開始,我們要做的事情很簡單,就是模擬打三隻 API,分別是以下幾個

1. 文章列表

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const posts = [
{
postId: 1,
title: 'post1',
},
{
postid: 2,
title: 'post2'
},
{
postid: 3,
title: 'post3'
}
]

2. 文章資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const postInfo = [
{
authorId: 1,
content: 'content',
createdAt: '2022-05-06'
},
{
authorId: 2,
content: 'content',
createdAt: '2022-05-07'
},
{
authorId: 3,
content: 'content',
createdAt: '2022-05-08'
}
]

3. 作者資訊

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const authors = [
{
id: 1,
name: "PeaNu",
email: "jimdevelopesite@gmail.com",
},
{
id: 2,
name: "Garry",
email: "garrylovecook@gmail.com",
},
{
id: 3,
name: "PPB",
email: "ppbissuperman@gmail.com",
}
]

順序的話就是:

  1. 從文章列表中取得文章 id,再用 id 查文章資訊
  2. 從文章資訊中取得作者 id,再用 id 查作者
  3. 拿到作者的名字

所以等一下會從 callback 介紹到 Promise,再介紹到 generator 三種不同的方式。

順道一提,因為是假資料,所以會用 setTimeout 來模擬非同步。

callback

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function getPosts (callback) {
setTimeout(() => callback(posts), 1000)
}

function getPostInfo (id, callback) {
setTimeout(() => callback(postInfo.find(item => item.authorId === id)), 1000)
}

function getAuthor (id, callback) {
setTimeout(() => callback(authors.find(item => item.id === id)), 1000)
}

getPosts(posts => {
getPostInfo(posts[0].postId, post => {
getAuthor(post.authorId, author => {
console.log(author.name);
})
})
})

俗稱的 callback hell,不過 JS 寫久以後就會覺得逐漸麻痺了,雖然真的蠻醜的。

Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function getPosts () {
return new Promise(resolve => {
setTimeout(() => resolve(posts), 1000)
})
}

function getPostInfo (id) {
return new Promise(resolve => {
setTimeout(() => resolve(postInfo.find(item => item.authorId === id)), 1000)
})
}

function getAuthor (id) {
return new Promise(resolve => {
setTimeout(() => resolve(authors.find(item => item.id === id)), 1000)
})
}

getPosts()
.then(posts => getPostInfo(posts[0].postId))
.then(post => getAuthor(post.authorId))
.then(author => console.log(author.name))

改用 return Promise 的方式來包裝,接著就可以用 then 來處理,避免了 callback hell 的問題。

generator

接著是重頭戲了,先來看 code:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 上面維持剛剛的 Promise,所以就不寫出來了
function* getResult () {
const posts = yield getPosts();
const post = yield getPostInfo(posts[0].postId);
const author = yield getAuthor(post.authorId);
console.log(author);
}

const iterable = getResult();
iterable.next().value.then(posts => {
iterable.next(posts).value.then(post => {
iterable.next(post).value.then(author => {
iterable.next(author);
console.log('done')
})
})
})

這邊只要理解下面那一段,就會知道為什麼可以這樣寫了,所以來一步一步看吧:

iterable.next()

這邊會跑到第一個 yield getPosts(),所以可以用 .then 拿到文章列表。

iterable.next(posts)

記得以前說的嗎?在 next 裡面傳參數就代表「把上一個 yield 的值改寫掉」,所以 const posts = response

接著因為 posts 有值了,所以 yield 後面的 getPostInfo(posts[0].postId) 就可以繼續往下執行。

iterable.next(post)

跟剛剛一樣,把 post 寫到上一個 yield,所以 const post = response,後面的 yield getAuthor(post.authorId) 正常跑。

iterable.next(author)

這邊也一樣,會把 author 的寫到上一個 yield,所以 const author = response

接著就繼續往下執行到 console.log(author),這時候因為上一行已經賦值了,所以就可以印出最後的結果。

這一段如果看不懂的話就重新思考 next(params) 的作用是什麼?然後一步一步照著程式來跑就會理解一些了。

總之這邊只是想示範一下用 generator 來實作非同步是什麼樣的感覺。

再更進階一點

剛剛的部分其實還可以再做個優化,畢竟要一直用 iterable.next 的話在某種意義上也有點 callback hell 的感覺,所以其實可以改寫成「遞迴」的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* getResult () {
const posts = yield getPosts();
const post = yield getPostInfo(posts[0].postId);
const author = yield getAuthor(post.authorId);
console.log(author);
}

function run () {
const iterable = getResult();
function go (result) {
// 只要 done 不是 true 就一直遞迴
if (result.done) return
result.value.then(res => go(iterable.next(res)))
}
go(iterable.next())
}

run();

這樣就有一個自動化的 function 來幫你跑 generator 了,是不是有一點 async/awiat 的感覺?也許這背後就是用這種方式來實作的也說不定。

總之,以上就是 generator 的示範,希望我能越來越熟悉這玩意兒。

希望是最好懂的 Redux Saga 快速入門 generator
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×