Skip to content

Instantly share code, notes, and snippets.

@msyfls123
Last active September 16, 2019 14:30
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save msyfls123/289725ebf567370b052198fb042e4c71 to your computer and use it in GitHub Desktop.
Save msyfls123/289725ebf567370b052198fb042e4c71 to your computer and use it in GitHub Desktop.

Reactive programming makes you see more and further

响应式编程使你看得更远

简介切入

现代 web 开发的痛点:从只是展示静态的页面,到富交互的 web app

障碍:网页元素重绘延迟/网络请求延迟/交互的响应延迟带来的复杂状态维护

重点:让一切变成更及时、同步、可交互

场景 1:连续发出的 fetch 请求

  1. 全局加锁,缺点是无法发出更新的请求,需要全局变量
let isLocked = false

function sync(url, data) {
  if(isLocked) { return }
  isLocked = true
  fetch(url, data).then((res) => {
    doSomething(res)
    isLocked = false
  })
}
  1. 竞争 race,丢弃老的请求,缺点是将判断条件写在请求内部,造成了不必要的耦合
useEffect(() => {
  let aborted = false
  fetch(url, data).then((res) => {
    if (aborted) { return }
    doSomething(res)
  }
  return function() {
    aborted = true
  }
}, [url, data])

ES6 在异步编程上的努力

Promise

  • 单一值,pipeable
  • 方法:resolve/reject then/catch/finally race/all/allSettled

Promise 是异步编程道路上的一次飞跃,resolve/reject 类似其他语言的 Result,但其处理复杂异步依赖关系上有障碍

Generator:Iterator

代码块获得了 pause/resume 的能力,可以使异步编程更像同步编程,但主要流程需要在函数内外调用 yield 和 next,不够简洁,需要搭配 thunk 使用

async/await

化异步为同步写法,直接返回 Promise,但仍需要 Promise 的方法来处理复杂异步关系

Reactive Programing

https://www.reactivemanifesto.org/

  • Responsive 即时响应性
  • Resilient 回弹性
  • Elastic 弹性
  • Message Driven 消息驱动

Rx.js

Promise++

  • Stream:离散的时间相关的序列
  • Observable(只读):被观察的 Stream
  • Observer:订阅(subscribe)Observable 的对象
  • Subject(可读可写):既是被观察对象也是观察者
  • Operators:操作符,惰性,纯函数,主要应用

解决场景 1

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 }))
  )
}

更复杂的实际场景 2

模拟带主从关系的轿车车窗运行

  1. 按住按钮车窗上升、下降
  2. 到达顶部/底部则停止运动
  3. 按住超过 2s 则松开后继续保持 运动
  4. 驾驶位按钮对车窗运行起决定作用

如何处理 互为依赖并发有边界有中间变量 的异步关系

  • 互为依赖、有边界:A <==> B 速度/位置 (递归)
  • 并发:同时按下上升或下降按钮(加锁)
  • 有中间变量:
    1. 时间序列(setTimeout, interval, requestAnimationTime)
    2. 车窗运动方向
    3. 按钮是否被按住了一段时间(???)

核心思路

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(!!!)

介绍 redux-observable、rxjs-hooks 和 Cycle.js

本质都是基于流的响应式组件,从一个状态经过一个事件转换到另一个状态

redux-observable

epic (action$, state$) => newAction$

rxjs-hooks

value = useObservable((inputs$, state$) => Observable, initialState, input[])

[callback, value] = useEventCallback((event$, inputs$, state$) => Observable, initialState, input[])

摆脱了 redux 顶层的依赖,方便用 rxjs 理清复杂 state 的依赖关系

Cycle.js

HCI 互为倾听者、倾诉者

state$(DOM, HTTPRequest, history) <==> action$(DOM Event)

总结

  • 响应式编程可以使我们更专注于事件的发生,事件的合并组合与转化,最终合成表现层
  • 尽可能地消除了无用的中间变量存取,每一个流都有其实际物理意义,更便于理解
  • 与语言弱相关,主要是编程范式,可以复用到其他领域
@msyfls123
Copy link
Author

中间变量倒也不是不可取,但若以流的形式存在的话,在初始化时就能知道这个流会流经哪些地方,又有哪些流会流经它,感觉会是一个更低风险和更少阅读成本的主意。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment