點燈坊

失くすものさえない今が強くなるチャンスよ

實務上 then() 與 await 在 Vue 使用方式

Sam Xiao's Avatar 2020-12-16

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