现代 web 开发的痛点:从只是展示静态的页面,到富交互的 web app
障碍:网页元素重绘延迟/网络请求延迟/交互的响应延迟
带来的复杂状态维护
重点:让一切变成更及时、同步、可交互
- 全局加锁,缺点是无法发出更新的请求,需要全局变量
let isLocked = false
function sync(url, data) {
if(isLocked) { return }
isLocked = true
fetch(url, data).then((res) => {
doSomething(res)
isLocked = false
})
}
- 竞争 race,丢弃老的请求,缺点是将判断条件写在请求内部,造成了不必要的耦合
useEffect(() => {
let aborted = false
fetch(url, data).then((res) => {
if (aborted) { return }
doSomething(res)
}
return function() {
aborted = true
}
}, [url, data])
- 单一值,pipeable
- 方法:resolve/reject then/catch/finally race/all/allSettled
Promise 是异步编程道路上的一次飞跃,resolve/reject 类似其他语言的 Result,但其处理复杂异步依赖关系上有障碍
代码块获得了 pause/resume 的能力,可以使异步编程更像同步编程,但主要流程需要在函数内外调用 yield 和 next,不够简洁,需要搭配 thunk 使用
化异步为同步写法,直接返回 Promise,但仍需要 Promise 的方法来处理复杂异步关系
https://www.reactivemanifesto.org/
- Responsive 即时响应性
- Resilient 回弹性
- Elastic 弹性
- Message Driven 消息驱动
Promise++
- Stream:离散的时间相关的序列
- Observable(只读):被观察的 Stream
- Observer:订阅(subscribe)Observable 的对象
- Subject(可读可写):既是被观察对象也是观察者
- Operators:操作符,惰性,纯函数,主要应用
const res$ = combineLatest(
url$,
data$
).pipe(
switchMap(([url, data]) => {
return from(fetch(url, data))
})
)
得到了一个永远随着 url/data 变化而更新最新值
的结果流
import { defer, of } from 'rxjs'
import { map, retry, catchError } from 'rxjs/operators'
function retryableRequest(url, data) {
return defer(() => fetch(url, data)).pipe(
retry(2),
map((res) => ({ type: 'SUCCESS', res })),
catchError((err) => of({ type: 'ERROR', err }))
)
}
模拟带主从关系的轿车车窗运行
- 按住按钮车窗上升、下降
- 到达顶部/底部则停止运动
- 按住超过 2s 则松开后继续保持 运动
- 驾驶位按钮对车窗运行起决定作用
如何处理 互为依赖
、并发
、有边界
、有中间变量
的异步关系
- 互为依赖、有边界:A <==> B 速度/位置 (递归)
- 并发:同时按下上升或下降按钮(加锁)
- 有中间变量:
- 时间序列(setTimeout, interval, requestAnimationTime)
- 车窗运动方向
- 按钮是否被按住了一段时间(???)
constant$ + action$ => state$
https://stackblitz.com/edit/car-window
步骤 | 所用操作符 |
---|---|
创建稳定的时间流 tick$(常量流) | interval |
创建按键流 key$(action$) | BehaviorSubject |
创建车窗的位置流 position$(state$) | BehaviorSubject |
通过按键流创建基础方向流 rawDir$ | scan/share |
通过 rawDir$ 加延时得到 忽略下次归零事件 的 ignore$ 流 |
switchMap/timer/takeUntil |
合并 4、5 的两个流,得到合成后的速度流 dir$ | merge/distinctUntilChanged/share |
根据时间和速度两个流,计算出位置流 | withLatestFrom/scan |
last but not least,考虑车窗运动到顶部和底部的情况,修正到 6 中的速度流 dir$ => direction$ | combineLatest/share(!!!) |
本质都是基于流的响应式组件,从一个状态经过一个事件转换到另一个状态
epic (action$, state$) => newAction$
value = useObservable((inputs$, state$) => Observable, initialState, input[])
[callback, value] = useEventCallback((event$, inputs$, state$) => Observable, initialState, input[])
摆脱了 redux 顶层的依赖,方便用 rxjs 理清复杂 state 的依赖关系
HCI 互为倾听者、倾诉者
state$(DOM, HTTPRequest, history) <==> action$(DOM Event)
- 响应式编程可以使我们更专注于事件的发生,事件的合并组合与转化,最终合成表现层
- 尽可能地消除了无用的中间变量存取,每一个流都有其实际物理意义,更便于理解
- 与语言弱相关,主要是编程范式,可以复用到其他领域
中间变量倒也不是不可取,但若以流的形式存在的话,在初始化时就能知道这个流会流经哪些地方,又有哪些流会流经它,感觉会是一个更低风险和更少阅读成本的主意。