ReactDnD如何處理拖拽詳解_第1頁
ReactDnD如何處理拖拽詳解_第2頁
ReactDnD如何處理拖拽詳解_第3頁
ReactDnD如何處理拖拽詳解_第4頁
ReactDnD如何處理拖拽詳解_第5頁
已閱讀5頁,還剩14頁未讀, 繼續(xù)免費閱讀

下載本文檔

版權說明:本文檔由用戶提供并上傳,收益歸屬內容提供方,若內容存在侵權,請進行舉報或認領

文檔簡介

第ReactDnD如何處理拖拽詳解目錄正文代碼結構DndProviderDragDropManageruseDragHTML5BackendTouchBackend總結

正文

ReactDnD是一個專注于數(shù)據(jù)變更的React拖拽庫,通俗的將,你拖拽改變的不是頁面視圖,而是數(shù)據(jù)。ReactDnD不提供炫酷的拖動體驗,而是通過幫助我們管理拖拽中的數(shù)據(jù)變化,再由我們根據(jù)這些數(shù)據(jù)進行渲染。我們可能需要寫額外的視圖層來完成想要的效果,但是這種拖拽管理方式非常的通用,可以在任何場景下使用。初次使用可能感覺并不是那么方便,但是如果場景比較復雜,或者是需要高度定制,ReactDnD一定是首選。

ReactDnD的使用說明可以參見官方文檔。本文分析ReactDnD的源碼,更深層次的了解這個庫。以下的代碼來源于react-dnd14.0.4。

代碼結構

React-DnD是單個代碼倉庫,但是打了多個包。這種方式也表示了ReactDnD的三層結構。

_____________________________________

||||||

|||||backend-html|

|react-dnd||dnd-core|||

|||||backend-touch|

|___________||___________||_______________|

react-dnd是React版本的DragandDrop的實現(xiàn)。它定義了DragSource,DropTarget,DragDropContext等高階組件,以及useDrag,useDrop等hook。我們可以簡單的理解為這是一個接入層。

dnd-core是整個拖拽庫的核心,它實現(xiàn)了一個和框架無關的拖放管理器,定義了拖放的交互,根據(jù)dnd-core中定義的規(guī)則,我們完全可以根據(jù)它自己實現(xiàn)一個vue-dnd。dnd-core中使用redux做狀態(tài)管理。

backend是ReactDnD抽象了后端的概念,這里是DOM事件轉換為reduxaction的地方。如果是H5應用,backend-html,如果是移動端,使用backend-touch。也支持用戶自定義。

DndProvider

如果想要使用ReactDnD,首先需要在外層元素上加一個DndProvider。

import{HTML5Backend}from'react-dnd-html5-backend';

import{DndProvider}from'react-dnd';

DndProviderbackend={HTML5Backend}

TutorialApp/

/DndProvider

DndProvider的本質是一個由React.createContext創(chuàng)建一個上下文的容器(組件),用于控制拖拽的行為,數(shù)據(jù)的共享。DndProvider的入?yún)⑹且粋€Backend。Backend是什么呢?ReactDnD將DOM事件相關的代碼獨立出來,將拖拽事件轉換為ReactDnD內部的reduxaction。由于拖拽發(fā)生在H5的時候是ondrag,發(fā)生在移動設備的時候是由touch模擬,ReactDnD將這部分單獨抽出來,方便后續(xù)的擴展,這部分就叫做Backend。它是DnD在Dom層的實現(xiàn)。

以下是DndProvider的核心代碼,通過入?yún)⑸梢粋€manager,這個manager用于控制拖拽行為。這個manager放到Provider中,子節(jié)點都可以訪問這個manager。

exportconstDndProvider:FCDndProviderPropsunknown,unknown=memo(

functionDndProvider({children,...props}){

const[manager,isGlobalInstance]=getDndContextValue(props)

returnDndContext.Providervalue={manager}{children}/DndContext.Provider

DragDropManager

DndProvider將DndProvider放到了context中,這個manager非常關鍵,后續(xù)的拖動都依賴于manager,如下是它的創(chuàng)建過程。

exportfunctioncreateDragDropManager(

backendFactory:BackendFactory,

globalContext:unknown=undefined,

backendOptions:unknown={},

debugMode=false,

):DragDropManager{

conststore=makeStoreInstance(debugMode)

constmonitor=newDragDropMonitorImpl(store,newHandlerRegistryImpl(store))

constmanager=newDragDropManagerImpl(store,monitor)

constbackend=backendFactory(manager,globalContext,backendOptions)

manager.receiveBackend(backend)

returnmanager

首先看下store的創(chuàng)建過程,manager中store的創(chuàng)建使用了redux的createStore方法,store是用來以存放應用中所有的state的。它的第一個參數(shù)reducer接收兩個參數(shù),分別是當前的state樹和要處理的action,返回新的state樹。

functionmakeStoreInstance():StoreState{

returncreateStore(reduce)

manager中的store管理著如下state,每個state都有對應的方法進行更新。

exportinterfaceState{

dirtyHandlerIds:DirtyHandlerIdsState

dragOffset:DragOffsetState

refCount:RefCountState

dragOperation:DragOperationState

stateId:StateIdState

標準的redux更新數(shù)據(jù)的方法是dispatchaction的方式。如下是dragOffset更新方法,判斷當前action的類型,從payload中獲得需要的參數(shù),然后返回新的state。

exportfunctionreduce(

state:State=initialState,

action:Action{

sourceClientOffset:XYCoord

clientOffset:XYCoord

):State{

const{payload}=action

switch(action.type){

caseINIT_COORDS:

caseBEGIN_DRAG:

return{

initialSourceClientOffset:payload.sourceClientOffset,

initialClientOffset:payload.clientOffset,

clientOffset:payload.clientOffset,

caseHOVER:

caseEND_DRAG:

caseDROP:

returninitialState

default:

returnstate

接下來看monitor,已知store表示的是拖拽過程中的數(shù)據(jù),那么我們可以根據(jù)這些數(shù)據(jù)計算出當前的一些狀態(tài),比如某個物體是否可以被拖動,某個物體是否正在懸空等等。monitor提供了一些方法來訪問這些數(shù)據(jù),不僅如此,monitor最大的作用是用來監(jiān)聽這些數(shù)據(jù)的,我們可以為monitor添加一些監(jiān)聽器,這樣在數(shù)據(jù)變動之后就能及時響應。

如下列出了一些monitor中的方法。

exportinterfaceDragDropMonitor{

subscribeToStateChange(

listener:Listener,

options:{

handlerIds:Identifier[]|undefined

):Unsubscribe

subscribeToOffsetChange(listener:Listener):Unsubscribe

canDragSource(sourceId:Identifier|undefined):boolean

canDropOnTarget(targetId:Identifier|undefined):boolean

isDragging():boolean

isDraggingSource(sourceId:Identifier|undefined):boolean

getItemType():Identifier|null

getItem():any

getSourceId():Identifier|null

getTargetIds():Identifier[]

getDropResult():any

didDrop():boolean

subscribeToStateChange就是添加監(jiān)聽函數(shù)的方法,其原理是使用了redux的subscribe方法。

publicsubscribeToStateChange(

listener:Listener,

options:{handlerIds:string[]|undefined}={handlerIds:undefined},

):Unsubscribe{

returnthis.store.subscribe(handleChange)

要注意的是,DragDropMonitor是一個全局的monitor,它監(jiān)聽的范圍是DndProvider下所有可拖拽的元素,也就是monitor中會存在多個對象,這些拖拽對象有全局唯一性的ID標識(從0自增的ID)。這也是monitor中的發(fā)部分方法都需要傳一個Identifier的原因。還有一點就是,最好不要存在多個DndProvider,除非你確定不同DndProvider下拖拽元素一定不會交互。

我們在DndProvider傳入了一個參數(shù)backend,其實它是個工廠方法,執(zhí)行之后會生成真正的backend。

manager比較簡單,它包含了之前生成的monitor,store,backend,還在初始化的時候為store添加了一個監(jiān)聽器。它監(jiān)聽state中的refCount方法,refCount表示當前標記為可拖拽的對象,如果refCount大于0,初始化backend,否則,銷毀backend。

exportclassDragDropManagerImplimplementsDragDropManager{

privatestore:StoreState

privatemonitor:DragDropMonitor

privatebackend:Backend|undefined

privateisSetUp=false

publicconstructor(store:StoreState,monitor:DragDropMonitor){

this.store=store

this.monitor=monitor

store.subscribe(this.handleRefCountChange)

privatehandleRefCountChange=():void={

constshouldSetUp=this.store.getState().refCount0

if(this.backend){

if(shouldSetUp!this.isSetUp){

this.backend.setup()

this.isSetUp=true

}elseif(!shouldSetUpthis.isSetUp){

this.backend.teardown()

this.isSetUp=false

manager創(chuàng)建完成,表示此時我們有了一個store來管理拖拽中的數(shù)據(jù),有了monitor來監(jiān)聽數(shù)據(jù)和控制行為,能通過manager進行注冊,可以通過backend將Dom事件轉換為action。接下來就能使用useDrag來創(chuàng)建一個真正的可拖拽對象了。

useDrag

一個元素想要被拖拽,Hooks的寫法如下,使用useDrag實現(xiàn)。useDrag的入?yún)⒑头祷刂悼梢詤⒖脊俜轿臋n,這里不加贅述。

import{DragPreviewImage,useDrag}from'react-dnd';

exportconstKnight:FC=()={

const[{isDragging},drag,preview]=useDrag(

()=({

type:ItemTypes.KNIGHT,

collect:(monitor)=({

isDragging:!!monitor.isDragging()

return(

DragPreviewImageconnect={preview}src={knightImage}/

ref={drag}

/div

在使用useDrag的時候,我們配置了入?yún)?,是一個函數(shù),這個函數(shù)的返回值就是配置參數(shù),useOptionalFactory就是使用useMemo將這個方法包了一層,避免重復調用。

exportfunctionuseDragDragObject,DropResult,CollectedProps(

specArg:FactoryOrInstance

DragSourceHookSpecDragObject,DropResult,CollectedProps

deps:unknown[],

):[CollectedProps,ConnectDragSource,ConnectDragPreview]{

//獲得配置參數(shù)

constspec=useOptionalFactory(specArg,deps)

//獲得manager中的monitor的包裝對象(DragSourceMonitor)

constmonitor=useDragSourceMonitorDragObject,DropResult()

//連接dom以及redux

constconnector=useDragSourceConnector(spec.options,spec.previewOptions)

//生成唯一id,封裝DragSource對象

useRegisteredDragSource(spec,monitor,connector)

return[

useCollectedProps(spec.collect,monitor,connector),

useConnectDragSource(connector),

useConnectDragPreview(connector),

原先在manager中的monitor類型是DragDropMonitor,看名字就知道,該monitor中的方法是結合了Drag和Drop兩種行為的,目前只是使用Drag,因此將monitor包裝一下,屏蔽Drop的行為。使其類型變?yōu)镈ragSourceMonitor。這就是useDragSourceMonitor做的事情,

exportfunctionuseDragSourceMonitorO,R():DragSourceMonitorO,R{

constmanager=useDragDropManager()

returnuseMemo(()=newDragSourceMonitorImpl(manager),[manager])

以上,我們有Backend控制Dom層級的行為,Store和Monitor控制數(shù)據(jù)層的變化,那如何讓Monitor知道現(xiàn)在要監(jiān)聽到底是哪個節(jié)點,還需要將這兩者連接起來,才能真正的讓Dom層和數(shù)據(jù)層保持一致,ReactDnD中使用connector來連接著兩者。

useDragSourceConnector方法中會new一個SourceConnector的實例,該實例會接受backend作為入?yún)?,SourceConnector實現(xiàn)了Connector接口。Connector中成員變量不多,最重要就是hooks對象,該對象用于處理ref的邏輯。

exportinterfaceConnector{

//獲得ref指向的Dom

hooks:any

//獲得dragSource

connectTarget:any

//dragSource唯一Id

receiveHandlerId(handlerId:Identifier|null):void

//重新連接dragSource和dom

reconnect():void

我們在例子中將ref屬性給到了一個useDrag的返回值。該返回值其實就是hooks中的dragSource方法。

exportfunctionuseConnectDragSource(connector:SourceConnector){

returnuseMemo(()=connector.hooks.dragSource(),[connector])

從dragSource方法可以看出,connector中將這個Dom節(jié)點維護在了dragSourceNode屬性上。

exportclassSourceConnectorimplementsConnector{

//wrapConnectorHooks判斷ref節(jié)點是否是合法的ReactElement,是的話,執(zhí)行回調方法

publichooks=wrapConnectorHooks({

dragSource:(

node:Element|ReactElement|Refany,

options:DragSourceOptions,

)={

//dragSourceRef和dragSourceNode賦值null

this.clearDragSource()

this.dragSourceOptions=options||null

if(isRef(node)){

this.dragSourceRef=nodeasRefObjectany

}else{

this.dragSourceNode=node

this.reconnectDragSource()

獲得節(jié)點后,調用this.reconnectDragSource(),該方法中,backend調用connectDragSource方法為該節(jié)點添加事件監(jiān)聽,后續(xù)會分析backend。

privatereconnectDragSource(){

constdragSource=this.dragSource

if(didChange){

this.dragSourceUnsubscribe=this.backend.connectDragSource(

this.handlerId,

dragSource,

this.dragSourceOptions,

現(xiàn)在還需要對Dom進行抽象,生成唯一ID,封裝為DragSource注冊到monitor上。

exportfunctionuseRegisteredDragSourceO,R,P(

spec:DragSourceHookSpecO,R,P,

monitor:DragSourceMonitorO,R,

connector:SourceConnector,

):void{

constmanager=useDragDropManager()

//生成DragSource

consthandler=useDragSource(spec,monitor,connector)

constitemType=useDragType(spec)

//useLayoutEffect

useIsomorphicLayoutEffect(

functionregisterDragSource(){

if(itemType!=null){

//DragSource注冊到monitor

const[handlerId,unregister]=registerSource(

itemType,

handler,

manager,

//更新唯一ID,觸發(fā)reconnect邏輯

monitor.receiveHandlerId(handlerId)

connector.receiveHandlerId(handlerId)

returnunregister

[manager,monitor,connector,handler,itemType],

DragSource實現(xiàn)以下幾個方法,這個幾個方法我們使用useDarg的時候可以配置同名函數(shù),這些配置的方法會被以下方法調用。

exportinterfaceDragSource{

beginDrag(monitor:DragDropMonitor,targetId:Identifier):void

endDrag(monitor:DragDropMonitor,targetId:Identifier):void

canDrag(monitor:DragDropMonitor,targetId:Identifier):boolean

isDragging(monitor:DragDropMonitor,targetId:Identifier):boolean

總結下useDarg做的事情,首先就是支持一些配置參數(shù),這是最基礎的,然后獲得Provider中的managre,對其中的一些對象進行包裝,屏蔽一些方法,增加一些參數(shù)。最重要的就是創(chuàng)建connector,在界面加載完畢后,connector通過ref的方式獲得Dom節(jié)點的實例,為該節(jié)點添加拖拽屬性和拖拽事件。同時根據(jù)配置參數(shù)和connector封裝DragSource對象,將其注冊到monitor中。

useDrop和useDrag的流程大同小異,大家可以自己看。

HTML5Backend

之前為DndProvider注入的參數(shù)HTML5Backend,其實是個工程方法,我們在DndProvider除了可以配置backend外,還可以配置backend的一些參數(shù),當然,backend的實現(xiàn)不同,傳參也不同。DragDropManager會根據(jù)這些參數(shù)初始化真正的backend。

exportconstHTML5Backend:BackendFactory=functioncreateBackend(

manager:DragDropManager,

context:HTML5BackendContext,

options:HTML5BackendOptions,

):HTML5BackendImpl{

returnnewHTML5BackendImpl(manager,context,options)

如下是Backend需要被實現(xiàn)的方法。

exportinterfaceBackend{

setup():void

teardown():void

connectDragSource(sourceId:any,node:any,options:any):Unsubscribe

connectDragPreview(sourceId:any,node:any,options:any):Unsubscribe

connectDropTarget(targetId:any,node:any,options:any):Unsubscribe

profile():Recordstring,number

setup是backend的初始化方法,teardown是backend銷毀方法。上文提到過,setup和teardown是在handleRefCountChange中執(zhí)行的。ReactDnD會在我們第一個使用useDrag或是useDrop的時候,執(zhí)行setup方法,而在它檢測到沒有任何地方在使用拖拽功能的時候,執(zhí)行teardown方法。

HTML5BackendImpl的setup方法中執(zhí)行如下方法,target默認狀態(tài)下指的是window。這里監(jiān)聽了所有的拖拽事件。這是典型的事件委托的方式,統(tǒng)一將拖拽事件的回調函數(shù)都綁定在window上,不僅能提高性能,而且極大的降低了事件銷毀的難度。

privateaddEventListeners(target:Node){

if(!target.addEventListener){

return

target.addEventListener(

'dragstart',

this.handleTopDragStartasEventListener,

target.addEventListener('dragstart',this.handleTopDragStartCapture,true)

target.addEventListener('dragend',this.handleTopDragEndCapture,true)

target.addEventListener(

'dragenter',

this.handleTopDragEnterasEventListener,

target.addEventListener(

'dragenter',

this.handleTopDragEnterCaptureasEventListener,

true,

target.addEventListener(

'dragleave',

this.handleTopDragLeaveCaptureasEventListener,

true,

target.addEventListener('dragover',this.handleTopDragOverasEventListener)

target.addEventListener('dragover',this.handleTopDragOverCapture,true)

target.addEventListener('drop',this.handleTopDropasEventListener)

target.addEventListener(

'drop',

this.handleTopDropCaptureasEventListener,

true,

HTML5Backend拖拽的監(jiān)聽函數(shù)就是獲得拖拽事件的對象,拿到相應的參數(shù)。HTML5Backend通過Manager拿到一個DragDropActions的實例,執(zhí)行其中的方法。DragDropActions本質就是根據(jù)參數(shù)將其封裝為一個action,最終通過redux的dispatch將action分發(fā),改變store中的數(shù)據(jù)。

exportinterfaceDragDropActions{

beginDrag(

sourceIds:Identifier[],

options:any,

):ActionBeginDragPayload|undefined

publishDragSource():SentinelAction|undefined

hover(targetIds:Identifier[],options:any):ActionHoverPayload

drop(options:any):void

endDrag():SentinelAction

我們看下connectDragSource方法。該方法用于將某個Node節(jié)點轉換為可拖拽節(jié)點,并且添加監(jiān)聽事件。

HTML5Backend使用HTML5拖放API實現(xiàn)。首先:為了把一個元素設置為可拖放,把draggable屬性設置為true。然后監(jiān)聽ondragstart事件,該事件在用戶開始拖動元素時觸發(fā)。至于selectstart,不用關心,是用來處理一些IE特殊情況的。

publicconnectDragSource(

sourceId:string,

node:Element,

options:any,

):Unsubscribe{

//設置draggable屬性

node.setAttribute('draggable','true')

//添加dragstart監(jiān)聽

node.addEventListener('dragstart',handleDragStart)

//添加selectstart監(jiān)聽

node.addEventListener('selectstart',handleS

溫馨提示

  • 1. 本站所有資源如無特殊說明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請聯(lián)系上傳者。文件的所有權益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網頁內容里面會有圖紙預覽,若沒有圖紙預覽就沒有圖紙。
  • 4. 未經權益所有人同意不得將文件中的內容挪作商業(yè)或盈利用途。
  • 5. 人人文庫網僅提供信息存儲空間,僅對用戶上傳內容的表現(xiàn)方式做保護處理,對用戶上傳分享的文檔內容本身不做任何修改或編輯,并不能對任何下載內容負責。
  • 6. 下載文件中如有侵權或不適當內容,請與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準確性、安全性和完整性, 同時也不承擔用戶因使用這些下載資源對自己和他人造成任何形式的傷害或損失。

評論

0/150

提交評論