前端开发在开发什么
大家在前端开发的过程中,可能会想过这样一个问题:前端开发究竟是在开发什么?
在我看来,前端开发的本质是让网页视图能够正确的相应相关事件。在这句话中有三个关键字:"网页视图","正确的相应"和"相关事件"。
"相关事件"可能包括页面点击,鼠标滑动,定时器,服务端请求等等,"正确的相应"意味着我们要根据相关的事件来修改一些状态,而"网页视图"就是我们前端开发中最熟悉的部分了。
按照这样的观点我们可以给出这样 视图 = 响应函数(事件)
的公式:
View = reactionFn(Event)
在前端开发中,需要被处理事件可以归类为以下三种: - 用户执行页面动作,例如 click, mousemove 等事件 - 远程服务端与本地的数据交互,例如 fetch, websocket - 本地的异步事件,例如 setTimeout, setInterval
![ef590400d723f18923e926bda7b2bcd2.png](https://i-blog.csdnimg.cn/blog_migrate/3caf85002d5f2d79dbf6750f87be0685.jpeg)
这样我们的公式就可以进一步推导为
View = reactionFn(UserEvent | Timer | Remote API)
应用中的逻辑处理
为了能够更进一步理解这个公式与前端开发的关系,我们以新闻网站举例,该网站有以下三个要求
- 单击刷新:单击 Button 刷新数据
- 勾选刷新:勾选 Checkbox 时自动刷新,否则停止自动刷新
- 下拉刷新:当用户从屏幕顶端下拉时刷新数据
如果从前端的角度分析,这三种需求分别对应着 - 单击刷新:click -> fetch - 勾选刷新:change -> (setInterval + clearInterval) -> fetch - 下拉刷新:(touchstart + touchmove + touchend) -> fetch
![608a9e8c8203a571dd2b0ba99c8125e1.png](https://i-blog.csdnimg.cn/blog_migrate/2a3e479e8e166f758dde657242bdb3e5.jpeg)
MVVM
在 MVVM 的模式下,对应上文的响应函数(reactionFn)会在 Model 与 ViewModel 或者 View 与 ViewModel 之间进行被执行,而事件(Event)会在 View 与 ViewModel 之间进行处理。
![18e7f949b82b640963a643f4592c4d20.png](https://i-blog.csdnimg.cn/blog_migrate/6bfa0b980d9da2613dcc7cc076b16579.jpeg)
MVVM 可以很好的抽象视图层与数据层,但是响应函数(reactionFn)会散落在不同的转换过程中,这会导致数据的赋值与收集过程难以进行精确追踪。另外因为事件(Event)的处理在该模型中与视图部分紧密相关,导致 View 与 ViewModel 之间对事件处理的逻辑复用困难。
Redux
在 Redux 最简单的模型下,若干个事件(Event)的组合会对应到一个 Actions 上,而 reducer 函数可以被直接认为与上文提到的响应函数(reactionFn)对应。
![ced271c5a76e2258335929856fba85c2.png](https://i-blog.csdnimg.cn/blog_migrate/7217f232b1a9f145f352ca9355c65e78.jpeg)
但是在 Redux 中 - State 只能用于描述中间状态,而不能描述中间过程 - Action 与 Event 的关系并非一一对应导致 State 难以追踪实际变化来源
响应式编程与 RxJS
维基百科中是这样定义响应式编程
在计算中,响应式编程或反应式编程(英语:Reactive programming)是一种面向数据流和变化传播的声明式编程范式。这意味着可以在编程语言中很方便地表达静态或动态的数据流,而相关的计算模型会自动将变化的值通过数据流进行传播。
以数据流维度重新考虑用户使用该应用的流程
- 点击按钮 -> 触发刷新事件 -> 发送请求 -> 更新视图
- 勾选自动刷新
- 手指触摸屏幕
- 自动刷新间隔 -> 触发刷新事件 -> 发送请求 -> 更新视图
- 手指在屏幕上下滑
- 自动刷新间隔 -> 触发刷新事件 -> 发送请求 -> 更新视图
- 手指在屏幕上停止滑动 -> 触发下拉刷新事件 -> 发送请求 -> 更新视图
- 自动刷新间隔 -> 触发刷新事件 -> 发送请求 -> 更新视图
- 关闭自动刷新
以 Marbles 图表示
![fcfe07b50aa722d3dc11d2638d4f6e26.png](https://i-blog.csdnimg.cn/blog_migrate/7213ab653e83c51ceac94fcf4d6af497.jpeg)
拆分上图逻辑,就会得到使用响应式编程开发当前新闻应用时的三个步骤:
- 定义源数据流
- 组合/转换数据流
- 消费数据流并更新视图
我们分别来进行详细描述
1. 定义源数据流
使用 RxJS,我们可以很方便的定义出各种 Event 数据流
- 单击操作:涉及
click
数据流
click$ = fromEvent<MouseEvent>(document.querySelector('button'), 'click');
- 勾选操作:涉及
change
数据流
change$ = fromEvent(document.querySelector('input'), 'change');
- 下拉操作:涉及
touchstart
,touchmove
与touchend
三个数据流
touchstart$ = fromEvent<TouchEvent>(document, 'touchstart');
touchend$ = fromEvent<TouchEvent>(document, 'touchend');
touchmove$ = fromEvent<TouchEvent>(document, 'touchmove');
- 定时刷新
interval$ = interval(5000);
- 服务端请求
fetch$ = fromFetch('https://randomapi.azurewebsites.net/api/users');
2. 组合/转换数据流
点击刷新事件流:在点击刷新时,我们希望短时间内多次点击只触发最后一次,这通过 RxJS 的 debounceTime
operator 就可以实现
![7a2760a069549ea641f28305a03bca58.png](https://i-blog.csdnimg.cn/blog_migrate/1c2f6c1183d65a13a06bcdb16dde8f84.png)
clickRefresh$ = this.click$.pipe(debounceTime(300));
自动刷新流:使用 RxJS 的 switchMap
与之前定义好的 interval$
数据流配合
![7ec9b74115676629866b663b3b90e612.png](https://i-blog.csdnimg.cn/blog_migrate/c82f6bfee7e81c7d4e88b2707b01d70e.jpeg)
autoRefresh$ = change$.pipe(
switchMap(enabled => (enabled ? interval$ : EMPTY))
);
下拉刷新流:结合之前定义好的 touchstart$
touchmove$
与 touchend$
数据流
![77c6168e0563be971c10caaf82cc295c.png](https://i-blog.csdnimg.cn/blog_migrate/e19d472a48b6a7c0a837e99c5259329f.jpeg)
pullRefresh$ = touchstart$.pipe(
switchMap(touchStartEvent =>
touchmove$.pipe(
map(touchMoveEvent => touchMoveEvent.touches[0].pageY - touchStartEvent.touches[0].pageY),
takeUntil(touchend$)
)
),
filter(position => position >= 300),
take(1),
repeat()
);
最后,我们通过 merge 函数将定义好的 clickRefresh$
autoRefresh$
与 pullRefresh$
合并,就得到了刷新数据流
![abb07c373a5fd4563472be7fe4d9a72f.png](https://i-blog.csdnimg.cn/blog_migrate/626b7d1102aad50f988071e238617595.jpeg)
refresh$ = merge(clickRefresh$, autoRefresh$, pullRefresh$));
3.消费数据流并更新视图
将刷新数据流直接通过 switchMap
打平到在第一步到定义好的 fetch$
,我们就获得了视图数据流
![89a3bbe1616c5d1066b444da0e91199b.png](https://i-blog.csdnimg.cn/blog_migrate/96b82bc07c4752b11460e9f7d58618f1.jpeg)
可以通过在 Angular 框架中可以直接 async pipe 将视图流直接映射为视图
<div *ngFor="let user of view$ | async">
</div>
在其他框架中可以通过 subscribe 获得数据流中的真实数据,再更新视图。
至此,我们就使用响应式编程完整的开发完成了当前新闻应用,示例代码由 Angular 开发,行数不超过 160 行。
我们总结一下,使用响应式编程思想开发前端应用时经历的三个过程与第一节中公式的对应关系
View = reactionFn(UserEvent | Timer | Remote API)
- 描述源数据流:与事件
UserEvent | Timer | Remote API
对应,在 RxJS 中对应函数分别是
- UserEvent: fromEvent
- Timer: interval, timer
- Remote API: fromFetch, webSocket
- 组合转换数据流:与响应函数(reactionFn)对应,在 RxJS 中对应的部分方法是
- COMBINING: merge, combineLatest, zip
- MAPPING: map
- FILTERING: filter
- REDUCING: reduce, max, count, scan
- TAKING: take, takeWhile
- SKIPPING: skip, skipWhile, takeLast, last
- TIME: delay, debounceTime, throttleTime
- 消费数据流更新视图,与 View 对应,在 RxJS 及 Angular 中可以使用
- subscribe
- async pipe
响应式编程相对于 MVVM 或者 Redux 有什么优点呢?
- - 描述事件发生的本身,而非计算过程或者中间状态
- - 提供了组合和转换数据流的方法,这也意味着我们获得了复用持续变化数据的方法
- - 由于所有数据流均由层层组合与转换获得,这也就意味着我们可以精确追踪事件及数据变化的来源
如果我们将 RxJS 的 Marbles 图的时间轴模糊,并在每次视图更新时增加纵切面,我们就会发现这样两件有趣的事情
![c90ebf8d98d925ed6c0b89f01f87b318.png](https://i-blog.csdnimg.cn/blog_migrate/fecde298b3647ea34c80cfc86f760be8.jpeg)
- Action 是 EventStream 的简化
- State 是 Stream 在某个时刻的对应
难怪我们可以在 Redux 官网中有这样一句话:如果你已经使用了 RxJS,很可能你不再需要 Redux 了
The question is: do you really need Redux if you already use Rx? Maybe not. It's not hard to re-implement Redux in Rx. Some say it's a two-liner using Rx.scan() method. It may very well be!
写到这里,我们对网页视图能够正确的相应相关事件
这句话是否可以进行进一步的抽象呢?
所有事件 --找到--> 相关事件 --做出--> 响应
而按时间顺序发生的事件,本质上就是数据流,进一步拓展就可变成
源数据流 --转换--> 中间数据流 --订阅--> 消费数据流
这正是响应式编程在前端能够完美工作的基础思想。但是该思想是否只在前端开发中有所应用呢?
答案是否定的,该思想不仅可以应用于前端开发,在后端乃至实时数据处理中都有着广泛的应用,在下一篇中我们将继续介绍。
本文视频版本:响应式编程与流式数据, 从 RxJS 到 Flink_哔哩哔哩 (゜-゜)つロ 干杯~-bilibili
示例代码:vthinkxie/ng-pull-refresh
在线演示地址:StackBlitz