摘要:相比之下,響應(yīng)式編程在解決此類問(wèn)題上有著得天獨(dú)厚的優(yōu)勢(shì)。當(dāng)然要加深對(duì)的理解還是得多多實(shí)戰(zhàn)。要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的拖拽,需要對(duì)等多個(gè)事件進(jìn)行觀察,并相應(yīng)地改變小方塊的位置。具體實(shí)現(xiàn)可以參見(jiàn)添加初始延遲需求在拖拽的實(shí)際應(yīng)用中,有時(shí)會(huì)希望有個(gè)初始延遲。
本文最初發(fā)布于我的個(gè)人博客:咀嚼之味
面對(duì)交互性很強(qiáng)、數(shù)據(jù)變化復(fù)雜的場(chǎng)景,傳統(tǒng)的前端開(kāi)發(fā)方式往往存在一些共有的問(wèn)題:1). UI 狀態(tài)與數(shù)據(jù)難以追蹤;2). 寫出的代碼可讀性很差,邏輯代碼分布離散。
相比之下,響應(yīng)式編程(Reactive Programming)在解決此類問(wèn)題上有著得天獨(dú)厚的優(yōu)勢(shì)。Vue、Mobx、RxJS 這些庫(kù)都是響應(yīng)式編程思想的結(jié)晶。
很多人在接觸到 RxJS 后會(huì)有一個(gè)共同的感覺(jué):這個(gè)庫(kù)雖然很強(qiáng)大,但奈何各種各樣的 operators 太多了,在實(shí)際場(chǎng)景中根本不知道怎么運(yùn)用!所以本文并不旨在闡釋響應(yīng)式編程的優(yōu)越性,而是通過(guò)循序漸進(jìn)的實(shí)例來(lái)展示 RxJS 常用 operators 的使用場(chǎng)景。如果你尚未入門 RxJS,推薦你可以先看看一位來(lái)自臺(tái)灣的前端工程師 Jerry Hong 寫的 30 天精通 RxJS 系列。不要被三十天這個(gè)標(biāo)題給嚇到啦,如果你有一些函數(shù)式編程的經(jīng)驗(yàn)的話,周末花一天時(shí)間就能看完。當(dāng)然要加深對(duì) RxJS 的理解還是得多多實(shí)戰(zhàn)。畢竟實(shí)踐出真知嘛!
本文不適合 未入門的新手 與 已精通的高手。如果你覺(jué)得你對(duì) RxJS 有了初步的認(rèn)識(shí),但掌握程度不高,可能這篇文章就比較適合你了。你可以嘗試跟著本文的三個(gè)實(shí)例自己先做做看,再對(duì)比一下本文給出的解決方案,相信你能對(duì) RxJS 有更深入的理解。注意,本文給出的解決方案并不一定是最優(yōu)的解決方案,如果你有什么改進(jìn)的建議,可以在文末留言,謝謝!
1. 簡(jiǎn)單的拖拽需求:給定一個(gè)小方塊,實(shí)現(xiàn)簡(jiǎn)單的拖拽功能,要求鼠標(biāo)在小方塊上按下后能夠拖著小方塊進(jìn)行移動(dòng);鼠標(biāo)放開(kāi)后,則運(yùn)動(dòng)停止。
要實(shí)現(xiàn)一個(gè)簡(jiǎn)單的拖拽,需要對(duì) mousedown, mousemove, mouseup 等多個(gè)事件進(jìn)行觀察,并相應(yīng)地改變小方塊的位置。
首先分析一下,為了相應(yīng)地移動(dòng)小方塊,我們需要知道的信息有:1). 小方塊被拖拽時(shí)的初始位置;2). 小方塊在被拖拽著移動(dòng)時(shí),需要移動(dòng)到的新位置。通過(guò) Marble Diagram 來(lái)描述一下我們的原始流與想要得到的流,其中最下面這個(gè)流就是我們想要用于更新小方塊位置的流。
mousedown : --d----------------------d--------- mousemove : -m--m-m-m--m--m---m-m-------m-m-m-- mouseup : ---------u---------------------u--- dragUpdate : ----m-m-m-------------------m-m----
簡(jiǎn)而言之,就是在一次 mousedown 和 mouseup 之間觸發(fā) mousemove 時(shí),更新小方塊的位置。要做到這一點(diǎn),最重要的操作符是 takeUntil,相關(guān)的偽代碼如下:
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----
其實(shí) switchMap 就是 map + switch 組合的簡(jiǎn)寫形式。當(dāng)然,我們還需要同時(shí)記錄一下初始位置并根據(jù)鼠標(biāo)移動(dòng)的距離來(lái)更新小方塊的位置,實(shí)際的實(shí)現(xiàn)代碼如下:
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 主要作用就是獲取和更新小方塊的位置。具體實(shí)現(xiàn)可以參見(jiàn) Codepen
2. 添加初始延遲需求:在拖拽的實(shí)際應(yīng)用中,有時(shí)會(huì)希望有個(gè)初始延遲。就像手機(jī)屏幕上的諸多 App 圖標(biāo),在你想要拖拽它們進(jìn)行排序時(shí),通常需要按住圖標(biāo)一小段時(shí)間,比如 200ms(如下圖所示),這時(shí)該如何操作呢?
為了演示方便,這里我們先定義一個(gè)簡(jiǎn)單的動(dòng)畫(huà),當(dāng)用戶鼠標(biāo)按下超過(guò)一定時(shí)間后,播放一個(gè)閃爍動(dòng)畫(huà):
.blink { animation: 0.4s linear blinking; } @keyframes blinking { 0% { opacity: 1; } 50% { opacity: 0; } 100% { opacity: 1; } }
此處我們只做一個(gè)簡(jiǎn)單的實(shí)現(xiàn):在用戶鼠標(biāo)按下時(shí)間超過(guò) 200ms 且在這 200ms 的時(shí)間內(nèi)沒(méi)有發(fā)生鼠標(biāo)移動(dòng)時(shí),認(rèn)為拖拽開(kāi)始。偽代碼如下:
mousedown.switchMap(() => $$.delay(200).takeUntil(mousemove))
其中,上面的 $$ 指的是一個(gè)新創(chuàng)建的流。為了得到更直觀的理解,使用多個(gè) Marble Diagram 來(lái)分段理解之前的偽代碼:
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----
在第一次鼠標(biāo)按下的 200ms 內(nèi),觸發(fā)了 mousemove 事件,所以第一次 mousedown 并沒(méi)有觸發(fā)一次 dragStart,而在第二次鼠標(biāo)按下的 200ms 內(nèi),并沒(méi)有觸發(fā) mousemove 事件,所以最后就引起了一次 dragStart。
結(jié)合之前的簡(jiǎn)單拖拽的實(shí)現(xiàn),代碼如下:
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 的代碼,主要就是用于觸發(fā)動(dòng)畫(huà)的。完整代碼見(jiàn) Codepen
3. 拖拽接龍需求:給定 n 個(gè)小方塊,要求拖拽第一個(gè)小方塊進(jìn)行移動(dòng),后續(xù)的小方塊能夠以間隔 0.1s 的時(shí)間跟著之前的小方塊進(jìn)行延遲模仿運(yùn)動(dòng)。
此例中,我們不再要求“初始延遲”,因此針對(duì)正在拖拽著的紅色小方塊,只要沿用第一個(gè)例子中的簡(jiǎn)單拖拽的方法,即可獲取我們需要改變方塊位置的事件流:
mousedown.switchMap(() => mousemove.takeUntil(mouseup))
然而我們?cè)撊绾我来涡薷亩鄠€(gè)方塊的位置呢?首先,可以先構(gòu)造一個(gè)流來(lái)按延遲時(shí)間依次取得我們想要改變的小方塊:
// 獲取所有小方塊,圖示的例子中給出的是 7 個(gè)小方塊 const boxes = document.getElementsByClassName("box") // 使用 zip 操作符構(gòu)造一個(gè)由 boxes 組成的流 const boxes$ = Rx.Observable.from([].slice.call(boxes, 0)) const delayBoxes$ = boxes$.zip(Rx.Observable.interval(100).startWith(0), (box) => box)
假定 7 個(gè) 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(() => { /* 此處更新各個(gè)小方塊的位置 */ }))
讓我們繼續(xù)著眼于 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 所示,我們可以借助流的力量從容地在合適的時(shí)機(jī)修改對(duì)應(yīng)的小方塊的位置。具體的實(shí)現(xiàn)代碼如下所示:
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()
完整的實(shí)現(xiàn)代碼見(jiàn) Codepen
小結(jié)
這篇文章介紹了關(guān)于拖拽的三個(gè)實(shí)際場(chǎng)景:
在簡(jiǎn)單拖拽的實(shí)例中,使用到了 takeUntil, switchMap 操作符;
需要添加初始延遲時(shí),我們額外使用到 delay 操作符;
在最后的拖拽接龍實(shí)例中,mergeMap 操作符和 zip + interval 的組合發(fā)揮了很大的作用
相信看完本文以后,你們能夠深刻體會(huì)到:結(jié)合 Marble Diagram 來(lái)理解 RxJS 的流是一個(gè)非常棒的方法!
最后大家可以思考一下:在第三個(gè)例子中,如果把 mergeMap 改為 switchMap 或者 concatMap 會(huì)發(fā)生什么?這是課后作業(yè)。下課!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/83611.html
摘要:官網(wǎng)地址聊天機(jī)器人插件開(kāi)發(fā)實(shí)例教程一創(chuàng)建插件在系統(tǒng)技巧使你的更加專業(yè)前端掘金一個(gè)幫你提升技巧的收藏集。我會(huì)簡(jiǎn)單基于的簡(jiǎn)潔視頻播放器組件前端掘金使用和實(shí)現(xiàn)購(gòu)物車場(chǎng)景前端掘金本文是上篇文章的序章,一直想有機(jī)會(huì)再次實(shí)踐下。 2道面試題:輸入U(xiǎn)RL按回車&HTTP2 - 掘金通過(guò)幾輪面試,我發(fā)現(xiàn)真正那種問(wèn)答的技術(shù)面,寫一堆項(xiàng)目真不如去刷技術(shù)文章作用大,因此刷了一段時(shí)間的博客和掘金,整理下曾經(jīng)被...
摘要:是一個(gè)基于可觀測(cè)數(shù)據(jù)流在異步編程應(yīng)用中的庫(kù)。正如官網(wǎng)所說(shuō),是基于觀察者模式,迭代器模式和函數(shù)式編程。它具有時(shí)間與事件響應(yīng)的概念。通知不再發(fā)送任何值。和通知可能只會(huì)在執(zhí)行期間發(fā)生一次,并且只會(huì)執(zhí)行其中的一個(gè)。 RxJS是一個(gè)基于可觀測(cè)數(shù)據(jù)流在異步編程應(yīng)用中的庫(kù)。 ReactiveX is a combination of the best ideas fromthe Observer p...
摘要:由于技術(shù)棧的學(xué)習(xí),筆者需要在原來(lái)函數(shù)式編程知識(shí)的基礎(chǔ)上,學(xué)習(xí)的使用。筆者在社區(qū)發(fā)現(xiàn)了一個(gè)非常高質(zhì)量的響應(yīng)式編程系列教程共篇,從基礎(chǔ)概念到實(shí)際應(yīng)用講解的非常詳細(xì),有大量直觀的大理石圖來(lái)輔助理解流的處理,對(duì)培養(yǎng)響應(yīng)式編程的思維方式有很大幫助。 showImg(https://segmentfault.com/img/bVus8n); [TOC] 一. 響應(yīng)式編程 響應(yīng)式編程,也稱為流式編程...
摘要:插件開(kāi)發(fā)前端掘金作者原文地址譯者插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。提供了與使用掌控異步前端掘金教你使用在行代碼內(nèi)優(yōu)雅的實(shí)現(xiàn)文件分片斷點(diǎn)續(xù)傳。 Vue.js 插件開(kāi)發(fā) - 前端 - 掘金作者:Joshua Bemenderfer原文地址: creating-custom-plugins譯者:jeneser Vue.js插件是為應(yīng)用添加全局功能的一種強(qiáng)大而且簡(jiǎn)單的方式。插....
閱讀 2521·2023-04-25 22:09
閱讀 1038·2021-11-17 17:01
閱讀 1580·2021-09-04 16:45
閱讀 2630·2021-08-03 14:02
閱讀 825·2019-08-29 17:11
閱讀 3261·2019-08-29 12:23
閱讀 1097·2019-08-29 11:10
閱讀 3287·2019-08-26 13:48