Functional Reactive Programming
谈一谈「防连点检测」的解决思路
10 January 2019
CJ Ting
CJ Ting
Functional Reactive Programming 简称为 FRP,是一种 编程范式,核心思想是使用 Observable 来表示异步信息流,通过对流进行变换和组合,产生我们想要的结果。
FRP 的优点是我们可以站在更高层的角度处理异步交互,不再需要使用各种变量追踪异步状态。
FRP 有几乎所有语言的 Library,以下代码使用 RxJS 为例。
2一个 Observable 就是一个流,用来表示一个无尽的序列,这个序列只有三种状态:产生值、产生错误,结束。
我们可以将流认为是一个 无尽的数组,常规的数组操作,例如 map
, reduce
, filter
等,都可以对流进行操作。
除此之外,还有很多方法用于对流进行变换组合,我们可以通过 RxMarbles 进行直观地感受。
4考虑一个常见的「自动补全」场景:用户输入的时候,实时反馈补全建议。
这个问题有如下点需要考虑:
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))
使用 FRP 解决问题遵循一个相同的思路:构造流,变换组合,得到想要的结果。
在这里,输入流是?感兴趣的结果流是?
7输入流自然是用户的输入事件。
我们首先获取到一个流表示「该发送请求了」。
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())
接下来我们监听上面的「发送请求流」,得到请求结果流。
对流进行变换的一个主要方法是 map
,根据旧值,产生一个新值。那么,如果产生的是一个流怎么办?大部分情况下,我们会希望这个子流的值出现在父流当中,这时我们使用 flatMap
方法即可。
这里我们使用一个方法:switchMap
,和 flatMap
相比,它只会处理父流的最新的值,丢弃之前的旧值。
注意,RxJS 会自动将 Promise 处理为一个流。
到了这里,我们监听结果流显示即可。
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)
考虑如下「防止连续点击」检测:
输入流是什么?想要的结果流又是什么?
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))
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("检测到连续相同点击"))
CJ Ting