摘要:相比之下,響應式編程在解決此類問題上有著得天獨厚的優勢。當然要加深對的理解還是得多多實戰。要實現一個簡單的拖拽,需要對等多個事件進行觀察,并相應地改變小方塊的位置。具體實現可以參見添加初始延遲需求在拖拽的實際應用中,有時會希望有個初始延遲。
本文最初發布于我的個人博客:咀嚼之味
面對交互性很強、數據變化復雜的場景,傳統的前端開發方式往往存在一些共有的問題:1). UI 狀態與數據難以追蹤;2). 寫出的代碼可讀性很差,邏輯代碼分布離散。
相比之下,響應式編程(Reactive Programming)在解決此類問題上有著得天獨厚的優勢。Vue、Mobx、RxJS 這些庫都是響應式編程思想的結晶。
很多人在接觸到 RxJS 后會有一個共同的感覺:這個庫雖然很強大,但奈何各種各樣的 operators 太多了,在實際場景中根本不知道怎么運用!所以本文并不旨在闡釋響應式編程的優越性,而是通過循序漸進的實例來展示 RxJS 常用 operators 的使用場景。如果你尚未入門 RxJS,推薦你可以先看看一位來自臺灣的前端工程師 Jerry Hong 寫的 30 天精通 RxJS 系列。不要被三十天這個標題給嚇到啦,如果你有一些函數式編程的經驗的話,周末花一天時間就能看完。當然要加深對 RxJS 的理解還是得多多實戰。畢竟實踐出真知嘛!
本文不適合 未入門的新手 與 已精通的高手。如果你覺得你對 RxJS 有了初步的認識,但掌握程度不高,可能這篇文章就比較適合你了。你可以嘗試跟著本文的三個實例自己先做做看,再對比一下本文給出的解決方案,相信你能對 RxJS 有更深入的理解。注意,本文給出的解決方案并不一定是最優的解決方案,如果你有什么改進的建議,可以在文末留言,謝謝!
1. 簡單的拖拽需求:給定一個小方塊,實現簡單的拖拽功能,要求鼠標在小方塊上按下后能夠拖著小方塊進行移動;鼠標放開后,則運動停止。
要實現一個簡單的拖拽,需要對 mousedown, mousemove, mouseup 等多個事件進行觀察,并相應地改變小方塊的位置。
首先分析一下,為了相應地移動小方塊,我們需要知道的信息有: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:
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
2. 添加初始延遲需求:在拖拽的實際應用中,有時會希望有個初始延遲。就像手機屏幕上的諸多 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
3. 拖拽接龍需求:給定 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 會發生什么?這是課后作業。下課!
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/83611.html
摘要:官網地址聊天機器人插件開發實例教程一創建插件在系統技巧使你的更加專業前端掘金一個幫你提升技巧的收藏集。我會簡單基于的簡潔視頻播放器組件前端掘金使用和實現購物車場景前端掘金本文是上篇文章的序章,一直想有機會再次實踐下。 2道面試題:輸入URL按回車&HTTP2 - 掘金通過幾輪面試,我發現真正那種問答的技術面,寫一堆項目真不如去刷技術文章作用大,因此刷了一段時間的博客和掘金,整理下曾經被...
摘要:由于技術棧的學習,筆者需要在原來函數式編程知識的基礎上,學習的使用。筆者在社區發現了一個非常高質量的響應式編程系列教程共篇,從基礎概念到實際應用講解的非常詳細,有大量直觀的大理石圖來輔助理解流的處理,對培養響應式編程的思維方式有很大幫助。 showImg(https://segmentfault.com/img/bVus8n); [TOC] 一. 響應式編程 響應式編程,也稱為流式編程...
摘要:插件開發前端掘金作者原文地址譯者插件是為應用添加全局功能的一種強大而且簡單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內優雅的實現文件分片斷點續傳。 Vue.js 插件開發 - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應用添加全局功能的一種強大而且簡單的方式。插....
閱讀 2511·2023-04-25 22:09
閱讀 1023·2021-11-17 17:01
閱讀 1559·2021-09-04 16:45
閱讀 2620·2021-08-03 14:02
閱讀 817·2019-08-29 17:11
閱讀 3255·2019-08-29 12:23
閱讀 1089·2019-08-29 11:10
閱讀 3281·2019-08-26 13:48