本文最初发布于个人我的博客:咀嚼之味javascript
面对交互性很强、数据变化复杂的场景,传统的前端开发方式每每存在一些共有的问题:1). UI 状态与数据难以追踪;2). 写出的代码可读性不好,逻辑代码分布离散。css
相比之下,响应式编程(Reactive Programming)在解决此类问题上有着得天独厚的优点。Vue、Mobx、RxJS 这些库都是响应式编程思想的结晶。html
不少人在接触到 RxJS 后会有一个共同的感受:这个库虽然很强大,但奈何各类各样的 operators 太多了,在实际场景中根本不知道怎么运用!因此本文并不旨在阐释响应式编程的优越性,而是经过按部就班的实例来展现 RxJS 经常使用 operators 的使用场景。若是你还没有入门 RxJS,推荐你能够先看看一位来自台湾的前端工程师 Jerry Hong 写的 30 天精通 RxJS 系列。不要被三十天这个标题给吓到啦,若是你有一些函数式编程的经验的话,周末花一天时间就能看完。固然要加深对 RxJS 的理解仍是得多多实战。毕竟实践出真知嘛!前端
本文不适合 未入门的新手 与 已精通的高手。若是你以为你对 RxJS 有了初步的认识,但掌握程度不高,可能这篇文章就比较适合你了。你能够尝试跟着本文的三个实例本身先作作看,再对比一下本文给出的解决方案,相信你能对 RxJS 有更深刻的理解。注意,本文给出的解决方案并不必定是最优的解决方案,若是你有什么改进的建议,能够在文末留言,谢谢!java
需求:给定一个小方块,实现简单的拖拽功能,要求鼠标在小方块上按下后可以拖着小方块进行移动;鼠标放开后,则运动中止。react
要实现一个简单的拖拽,须要对 mousedown
, mousemove
, mouseup
等多个事件进行观察,并相应地改变小方块的位置。es6
首先分析一下,为了相应地移动小方块,咱们须要知道的信息有:1). 小方块被拖拽时的初始位置;2). 小方块在被拖拽着移动时,须要移动到的新位置。经过 Marble Diagram 来描述一下咱们的原始流与想要获得的流,其中最下面这个流就是咱们想要用于更新小方块位置的流。编程
mousedown : --d----------------------d--------- mousemove : -m--m-m-m--m--m---m-m-------m-m-m-- mouseup : ---------u---------------------u--- dragUpdate : ----m-m-m-------------------m-m----
简而言之,就是在一次 mousedown
和 mouseup
之间触发 mousemove
时,更新小方块的位置。要作到这一点,最重要的操做符是 takeUntil,相关的伪代码以下:前端工程师
mousedown.switchMap(() => mousemove.takeUntil(mouseup))
将 switchMap 和 takeUntil 加入上面的 Marble Diagram:ide
mousedown : --d----------------------d--------- mousemove : -m--m-m-m--m--m---m-m-------m-m-m-- mouseup : ---------u---------------------u--- stream1$ = mousedown.map(() => mousemove.takeUntil(mouseup)) stream1$ : --d----------------------d--------- \ \ m-m-m| -m-m| dragUpdate = stream1$.switch() dragUpdate : ----m-m-m-------------------m-m----
其实 switchMap 就是 map + switch 组合的简写形式。固然,咱们还须要同时记录一下初始位置并根据鼠标移动的距离来更新小方块的位置,实际的实现代码以下:
const box = document.getElementById('box') const mouseDown$ = Rx.Observable.fromEvent(box, 'mousedown') const mouseMove$ = Rx.Observable.fromEvent(document, 'mousemove') const mouseUp$ = Rx.Observable.fromEvent(document, 'mouseup') mouseDown$.map((event) => ({ pos: getTranslate(box), event, })) .switchMap((initialState) => { const initialPos = initialState.pos const { clientX, clientY } = initialState.event return mouseMove$.map((moveEvent) => ({ x: moveEvent.clientX - clientX + initialPos.x, y: moveEvent.clientY - clientY + initialPos.y, })) .takeUntil(mouseUp$) }) .subscribe((pos) => { setTranslate(box, pos) })
其中,getTranslate
和 setTranslate
主要做用就是获取和更新小方块的位置。具体实现能够参见 Codepen
需求:在拖拽的实际应用中,有时会但愿有个初始延迟。就像手机屏幕上的诸多 App 图标,在你想要拖拽它们进行排序时,一般须要按住图标一小段时间,好比 200ms(以下图所示),这时该如何操做呢?
为了演示方便,这里咱们先定义一个简单的动画,当用户鼠标按下超过必定时间后,播放一个闪烁动画:
.blink { animation: 0.4s linear blinking; } @keyframes blinking { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }
此处咱们只作一个简单的实现:在用户鼠标按下时间超过 200ms 且在这 200ms 的时间内没有发生鼠标移动时,认为拖拽开始。伪代码以下:
mousedown.switchMap(() => $$.delay(200).takeUntil(mousemove))
其中,上面的 $$
指的是一个新建立的流。为了获得更直观的理解,使用多个 Marble Diagram 来分段理解以前的伪代码:
mousedown : --d----------------------d--------- mousemove : -m---m----m--------m-------------m- stream1$ = mousedown.map(() => $$.delay(200).takeUntil(mousemove)) stream1$ : --d----------------------d--------- \ \ -| ----s| dragStart = mousedown.switchMap(() => $$.delay(200).takeUntil(mousemove)) dragStart : -------------------------------s----
在第一次鼠标按下的 200ms 内,触发了 mousemove
事件,因此第一次 mousedown
并无触发一次 dragStart,而在第二次鼠标按下的 200ms 内,并无触发 mousemove
事件,因此最后就引发了一次 dragStart。
结合以前的简单拖拽的实现,代码以下:
mouseDown$.switchMap((event) => { return Rx.Observable.of({ pos: getTranslate(box), event, }) .delay(200) .takeUntil(mouseMove$) }) .switchMap((initialState) => { const initialPos = initialState.pos const { clientX, clientY } = initialState.event box.classList.add('blink') return mouseMove$.map((moveEvent) => ({ x: moveEvent.clientX - clientX + initialPos.x, y: moveEvent.clientY - clientY + initialPos.y, })) .takeUntil(mouseUp$.do(() => box.classList.remove('blink'))) }) .subscribe((pos) => { setTranslate(box, pos) })
其中,多了两句操做 #box
的 classname 的代码,主要就是用于触发动画的。完整代码见 Codepen
需求:给定 n 个小方块,要求拖拽第一个小方块进行移动,后续的小方块可以以间隔 0.1s 的时间跟着以前的小方块进行延迟模仿运动。
此例中,咱们再也不要求“初始延迟”,所以针对正在拖拽着的红色小方块,只要沿用第一个例子中的简单拖拽的方法,便可获取咱们须要改变方块位置的事件流:
mousedown.switchMap(() => mousemove.takeUntil(mouseup))
然而咱们该如何依次修改多个方块的位置呢?首先,能够先构造一个流来按延迟时间依次取得咱们想要改变的小方块:
// 获取全部小方块,图示的例子中给出的是 7 个小方块 const boxes = document.getElementsByClassName('box') // 使用 zip 操做符构造一个由 boxes 组成的流 const boxes$ = Rx.Observable.from([].slice.call(boxes, 0)) const delayBoxes$ = boxes$.zip(Rx.Observable.interval(100).startWith(0), (box) => box)
假定 7 个 boxes 在 Marble Diagram 中分别表示为 a
, b
, c
, d
, e
, f
, g
:
boxes$ : (abcdefg)| interval(100) : 0---0---1---2---3---4---5---6---7---8--- delayBoxes$ = boxes$.zip(Rx.Observable.interval(100).startWith(0), (box) => box) delayBoxes$ : a---b---c---d---e---f---g|
只要将本来用于修改方块位置的 mousemove 事件流 mergeMap 到上面例子中的 delayBoxes$ 上,便可完成“拖拽接龙”。伪代码以下所示:
mousedown.switchMap(() => mousemove.takeUntil(mouseup)) .mergeMap(() => delayBoxes$.do(() => { /* 此处更新各个小方块的位置 */ }))
让咱们继续着眼于 Marble Diagram:
delayBoxes$ : ---a---b---c---d---e---f---g| dragUpdate$ : -----m--------m----------m------- stream1$ = dragUpdate$.map(() => delayBoxes$) stream1$ : -----m-------m----------m------- \ \ \ \ \ a---b---c---d---e---f---g| \ a---b---c---d---e---f---g| a---b---c---d---e---f---g| result$ = dragUpdate$.mergeMap(() => delayBoxes$) result$ : ---------a---b--ac--bd--cea-dfb-egc-f-d-g-e---f---g|
正如上面 Marble Diagram 所示,咱们能够借助流的力量从容地在合适的时机修改对应的小方块的位置。具体的实现代码以下所示:
const headBox = document.getElementById('head') const boxes = document.getElementsByClassName('box') const mouseDown$ = Rx.Observable.fromEvent(headBox, 'mousedown') const mouseMove$ = Rx.Observable.fromEvent(document, 'mousemove') const mouseUp$ = Rx.Observable.fromEvent(document, 'mouseup') const delayBoxes$ = Rx.Observable.from([].slice.call(boxes, 0)) .zip(Rx.Observable.interval(100).startWith(0), (box) => box) mouseDown$.map((e) => { const pos = getTranslate(headBox) return { pos, event: e, } }) .switchMap((initialState) => { const initialPos = initialState.pos const { clientX, clientY } = initialState.event return mouseMove$.map((moveEvent) => ({ x: moveEvent.clientX - clientX + initialPos.x, y: moveEvent.clientY - clientY + initialPos.y, })) .takeUntil(mouseUp$) }) .mergeMap((pos) => { return delayBoxes$.do((box) => { setTranslate(box, pos) }) }) .subscribe()
完整的实现代码见 Codepen
这篇文章介绍了关于拖拽的三个实际场景:
在简单拖拽的实例中,使用到了 takeUntil
, switchMap
操做符;
须要添加初始延迟时,咱们额外使用到 delay
操做符;
在最后的拖拽接龙实例中,mergeMap
操做符和 zip + interval
的组合发挥了很大的做用
相信看完本文之后,大家可以深入体会到:结合 Marble Diagram 来理解 RxJS 的流是一个很是棒的方法!
最后你们能够思考一下:在第三个例子中,若是把 mergeMap
改成 switchMap
或者 concatMap
会发生什么?这是课后做业。下课!