點燈坊

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

如何每隔一段時間才取得 Input 值 ?

Sam Xiao's Avatar 2020-06-09

若想 Input 輸入值就直接打 API,而不要透過 Button Click,傳統會寫在 input Event,但若想每隔一段時間才取得 Input 值減少 API Request,也就是所謂 Debounce,自行要實作就沒那麼簡單,但透過 RxJS 可簡單實現。

Version

macOS Catalina 10.15.4
WebStorm 2020.1.2
Vue 2.6.11
RxJS 6.5.5

Browser

debounce000

當 input 輸入後,會立即根據輸入值打 API 取得值顯示,不需透過 button click。

Event Model

<template>
  <div>
    <input type="text" @input="onInput">
    {{ category }}
  </div>
</template>

<script>
import axios from 'axios'

let fetchCategory = x => axios.get(`http://localhost:3000/categories/${x}`)
  .then(x => x.data.value)
  .catch(_ => 'N/A')

let onInput = function() {
  let { target: { value: id }} = event

  fetchCategory(id).then(
    x => this.category = x
  )
}

export default {
  name:'App',
  data: () => ({
    category: ''
  }),
  methods: {
    onInput
  }
}
</script>

第 3 行

<input type="text" @input="onInput">
{{ category }}

傳統會使用 Event Model,將 input event 使用 onInput() 處理,結果由 category data 顯示。

15 行

let onInput = function() {
  let { target: { value: id }} = event

  fetchCategory(id).then(
    x => this.category = x
  )
}

event.target.value 取得輸入 id,再呼叫 fetchCategory() 取得 category,由於回傳是 Promise,因此必須使用 then() 寫入 category side effect 使其在 HTML template 顯示。

這是典型 Vue 寫法,但若想每隔一段時間才取得 input 值,也就是要自行處理 debounce,這就沒這麼簡單了

Stream Model

<template>
  <div>
    <input type="text" v-stream:input="input$">
    {{ category$ }}
  </div>
</template>

<script>
import { of } from 'rxjs'
import { ajax } from 'rxjs/ajax'
import { pluck, debounceTime, exhaustMap, catchError } from 'rxjs/operators'

let fetchCategory$ = x => ajax(`http://localhost:3000/categories/${x}`).pipe(
  pluck('response', 'category'),
  catchError(_ => of('N/A'))
)

let subscriptions = function() {
  let category$ = this.input$.pipe(
    debounceTime(500),
    pluck('event', 'target', 'value'),
    exhaustMap(fetchCategory$)
  )

  return { category$ }
}

export default {
  name:'App',
  domStreams: ['input$'],
  subscriptions
}
</script>

第 3 行

<input type="text" v-stream:input="input$">
{{ category$ }}

RxJS 則使用 Stream Model,將 input event 直接轉成 input$ stream。

13 行

let fetchCategory$ = x => ajax(`http://localhost:3000/categories/${x}`).pipe(
  pluck('response', 'category'),
  catchError(_ => of('N/A'))
)

fetchCategory() 也由原本回傳 Promise 改成回傳 Observable。

19 行

let category$ = this.input$.pipe(
  debounceTime(500),
  pluck('event', 'target', 'value'),
  exhaustMap(fetchCategory$)
)

RxJS 提供了 debounceTime() operator,可直接對 input stream 做 debounce,由於 input stream 是 Observable,因此使用 exhaustMap() 讀取 API 避免 Observable of Observable。

直接回傳 category$ 供 HTML template 顯示。

debounceTime()
每隔一段時間才過濾 Observable

debounce001

Conclusion

  • Debounce 是 RxJS 經典應用,若用 Event Model 將很難實現,但若改用 Stream Model 思考則相當直覺

Reference

Kartik Jagdale, RxJS: Reducing number of API Calls to your server using debounceTime()
RxJS, debounceTime()