Functional Reactive Programming

谈一谈「防连点检测」的解决思路

10 January 2019

CJ Ting

什么是 Functional Reactive Programming ?

Functional Reactive Programming 简称为 FRP,是一种 编程范式,核心思想是使用 Observable 来表示异步信息流,通过对流进行变换和组合,产生我们想要的结果。

FRP 的优点是我们可以站在更高层的角度处理异步交互,不再需要使用各种变量追踪异步状态。

FRP 有几乎所有语言的 Library,以下代码使用 RxJS 为例。

2

Observable

一个 Observable 就是一个流,用来表示一个无尽的序列,这个序列只有三种状态:产生值、产生错误,结束。

3

对流进行变换

我们可以将流认为是一个 无尽的数组,常规的数组操作,例如 map, reduce, filter 等,都可以对流进行操作。

除此之外,还有很多方法用于对流进行变换组合,我们可以通过 RxMarbles 进行直观地感受。

4

一个简单的例子

考虑一个常见的「自动补全」场景:用户输入的时候,实时反馈补全建议。

这个问题有如下点需要考虑:

5

常规方案

      let currentQuery = null
      let lastQuery = null
      let lastResult = null

      input.addEventListener("keyup", () => {
        currentQuery = input.value
        hideSuggestions()
      })

      input.addEventListener("keydown", _.debounce(() => {
        const query = input.value
        if(query.length <= 2) return
        if(query === lastQuery) return showSuggestions(lastResult)
        lastQuery = query
        getSuggestions(query)
          .then(result => {
            if(currentQuery === query) {
              lastResult = result
              showSuggestions(result)
            }
          })
      }, 500))
6

FRP 方案

使用 FRP 解决问题遵循一个相同的思路:构造流,变换组合,得到想要的结果。

在这里,输入流是?感兴趣的结果流是?

7

FRP(1)

输入流自然是用户的输入事件。

我们首先获取到一个流表示「该发送请求了」。

      const clicks= fromEvent(input, "keyup")

      const sendRequest = clicks
        .pipe(debounceTime(500))
        .pipe(map(evt => evt.target.value))
        .pipe(filter(x => x.length > 2))
        .pipe(distinctUntilChanged())
8

FRP(2)

接下来我们监听上面的「发送请求流」,得到请求结果流。

对流进行变换的一个主要方法是 map,根据旧值,产生一个新值。那么,如果产生的是一个流怎么办?大部分情况下,我们会希望这个子流的值出现在父流当中,这时我们使用 flatMap 方法即可。

这里我们使用一个方法:switchMap,和 flatMap 相比,它只会处理父流的最新的值,丢弃之前的旧值。

注意,RxJS 会自动将 Promise 处理为一个流。

9

FRP(3)

到了这里,我们监听结果流显示即可。

      const clicks= fromEvent(input, "keyup")

      const sendRequest = clicks
        .pipe(debounceTime(500))
        .pipe(map(evt => evt.target.value))
        .pipe(filter(x => x.length > 2))
        .pipe(distinctUntilChanged())

      sendRequest
        .pipe(switchMap(getSuggestions))
        .subscribe(showSuggestions)

      clicks.subscribe(hideSuggestions)
10

防连点问题

考虑如下「防止连续点击」检测:

输入流是什么?想要的结果流又是什么?

11

某个时间段内的点击次数

输入自然是点击事件,我们希望输出一个流告诉我们:用户点击次数超过指标了。

clicks
  .pipe(mapTo(1))
  .pipe(scan((acc, one) => acc + one, 0))
  .pipe(sampleTime(3000))
  .pipe(startWith(0))
  .pipe(pairwise())
  .pipe(map(([prev, next]) => next - prev))
  .pipe(filter(x => x > 3))
  .subscribe(x => console.log("检测到多次点击!", x))
12

连续相同时间间隔的点击

clicks
  .pipe(map(_ => Date.now()))
  .pipe(pairwise())
  .pipe(map(([prev, next]) => next - prev))
  .pipe(pairwise())
  .pipe(map(([prev, next]) => Math.abs(prev - next) <= 50))
  .pipe(bufferCount(10))
  .pipe(map(items => items.every(x => x === true)))
  .subscribe(() => console.log("检测到连续相同点击"))
13

Thank you

CJ Ting

Use the left and right arrow keys or click the left and right edges of the page to navigate between slides.
(Press 'H' or navigate to hide this message.)