很有意思的寫法。
簡述
這篇是繼 快速入門 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", } ]
|
順序的話就是:
- 從文章列表中取得文章 id,再用 id 查文章資訊
- 從文章資訊中取得作者 id,再用 id 查作者
- 拿到作者的名字
所以等一下會從 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
| 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) { if (result.done) return result.value.then(res => go(iterable.next(res))) } go(iterable.next()) }
run();
|
這樣就有一個自動化的 function 來幫你跑 generator 了,是不是有一點 async/awiat
的感覺?也許這背後就是用這種方式來實作的也說不定。
總之,以上就是 generator 的示範,希望我能越來越熟悉這玩意兒。