ECMAScript 2015 提出了 Promise 後,提供了 then()
用法,而 ECMAScript 2017 又提出了 await
用法,但 Vue 該如何活用 then()
與 await
呢 ?
Version
Vue 2.6.11
Side Effect
<template>
<div>
<ul>
<li v-for="x in books" :key="x">
{{ x }}
</li>
</ul>
</div>
</template>
<script>
let fetchAPI = async () => [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
let mounted = function() {
fetchAPI()
.then(x => {
let result = x
.filter(y => y.price > 100)
.sort((y, z) => z.price - y.price)
.map(y => `${ y.title }: ${ y.price }`)
this.books = result
})
}
export default {
name:'App',
data:() => ({
books:[]
}),
mounted
}
</script>
12 行
let fetchAPI = async () => [
{ title: 'FP in JavaScript', price: 100 },
{ title: 'RxJS in Action', price: 200 },
{ title: 'Speaking JavaScript', price: 300 }
]
fetchAPI()
模擬一般 API 回傳 Object Array 的 Promise。
18 行
let mounted = function() {
fetchAPI()
.then(x => {
let result = x
.filter(y => y.price > 100)
.sort((y, z) => z.price - y.price)
.map(y => `${ y.title }: ${ y.price }`)
this.books = result
})
}
由於 fetchAPI()
回傳 Promise,必須在 then()
才能拿到 Promise 內部資料,因此很多人會直接在 then()
的 callback 寫一堆 code,無論是整理 data,或者是 side effect 到 Vue,通通寫在同一個 callback 中,這樣雖然可行,但老實說並不是 Promise 最佳寫法。
Async Await
let mounted = async function() {
let result = await fetchAPI()
this.books = result
.filter(x => x.price > 100)
.sort((x, y) => y.price - z.price)
.map(x => `${ x.title }: ${ x.price }`)
}
自從 ES2017 支援 async await
後,對於很多不熟 Promise 的人是一大救星,看到 Promise 只要使用 await
就可取得 Promise 內部值,剩下就可繼續使用 Imperative 與 side effect 寫法,唯一代價就是要宣告成 async function。
但老實說這也不是 await
最佳寫法。
let mounted = async function() {
this.books = await fetchAPI()
.then(x => x
.filter(y => y.price > 100)
.sort((y, z) => z.price - y.price)
.map(y => `${y.title}: ${y.price}`)
)
}
await
最佳寫法應該扮演 side effect 終結角色,也就是先用 then()
以 pure function 改變 Promise 內部,最後以 await
從 Promise 取出以 side effect 修改 Vue。
如此可明確分辨 then()
內是 pure function,而 await
專心處理 side effect,這才是 await
最佳使用方式。
then()
let mounted = function() {
fetchAPI()
.then(x => x
.filter(y => y.price > 100)
.sort((y, z) => z.price - y.price)
.map(y => `${y.title}: ${y.price}`)
)
.then(x => this.books = x)
}
不過使用 await
代價是所有 call stack 的 function 都要改成 async function。
實務上建議以最後一個 then()
專職處理 side effect,如此就不用宣告成 async function,這才是 then()
最佳使用方式。
Promise Chain
let mounted = function() {
fetchAPI()
.then(x => x.filter(y => y.price > 100))
.then(x => x.sort((y, z) => z.price - y.price))
.then(x => x.map(y => `${ y.title }: ${ y.price }`))
.then(x => this.books = x)
}
其實 Promise 有 Functor 特性,其 then()
類似於 Functor 的 map()
,因此可以在每個 then()
提供 pure function 去改變 Promise 內部值,如此每個 then()
就能單一職責處理一件事情,語義會更清楚。
Point-free
let mounted = function() {
fetchAPI()
.then(filter(x => x.price > 100))
.then(sort((x, y) => y.price - x.price))
.then(map(x => `${ x.title }: ${ x.price }`))
.then(x => this.books = x)
}
不過由於 then()
需要 callback,而 Array 的 method 也需要 callback,兩層 callback 並不好看,因此可使用 Ramda 的 function 使 then()
能 Point-free。
let mounted = function() {
fetchAPI()
.then(filter(propGt('price', 100)))
.then(sort(descend(prop('price'))))
.then(map(x => `${ x.title }: ${ x.price }`))
.then(x => this.books = x)
}
filter()
、sort()
與 map()
的 callback 也能進一步 Point-free。
map()
的 callback 因為較難 Point-free,暫時維持 arrow function,實務上應盡量 Point-free 減少 argument,除非遇到 Point-free 很難寫時
Composition Law
let transform = pipe(
filter(propGt('price', 100)),
sort(descend(prop('price'))),
map(x => `${ x.title }: ${ x.price }`)
)
let mounted = function() {
fetchAPI()
.then(transform)
.then(x => this.books = x)
}
由於 Promise 支援 composition law,其實可將 then()
所有的 pure function 都使用 pipe()
組合起來,一次傳給 then()
即可。
一樣最後一個 then()
專職處理 side effect。
IIFE
let transform = pipe(
filter(propGt("price", 100)),
sort(descend(prop("price"))),
map(x => `${ x.title }: ${ x.price }`)
)
let mounted = function() {
pipe(
fetchAPI,
then(transform),
then(x => this.books = x)
)()
}
若不想以 Method Chaining 方式使用 then()
,也可改用 Ramda 的 then()
,如此 fetchAPI()
也能使用 pipe()
組合起來,最後透過 IIFE 執行。
Conclusion
- 實務上不建議將處理 data 與 side effect 全部寫在
then()
的 callback,也不建議立即將 Promise 透過await
取出以 Imperative 與 side effect 方式處理,這兩種方式都很常見,但都不是 Promise 最佳寫法 - Promise 最佳寫法是先透過
then()
處理 Promise 內部資料,最後以await
終結處理 side effect,或者最後一個then()
處理 Promise,如此 pure function 與 side effect 分開,可將 side effect 降到最低 - 以 Promise chain 處理 data 也不錯,最少每一個
then()
都搭配明確的 pure function - 實務上建議以 Ramda 的
pipe()
組合提供then()
所需的 pure function,如此then()
的 callback 都能 Point-free - Promise 其實支援 composition law,藉由此特性可將 Promise Chain 的 pure function 先透過
pipe()
組合起來一次送給then()
,如此語意更佳 - Promise 的
then()
也可改用 Ramda 的then()
,如此可以 Function Pipeline 方式使用 Promise Chain