您现在的位置是:网站首页> 编程资料编程资料

React DnD如何处理拖拽详解_React_

2023-05-24 413人已围观

简介 React DnD如何处理拖拽详解_React_

正文

React DnD 是一个专注于数据变更的 React 拖拽库,通俗的将,你拖拽改变的不是页面视图,而是数据。React DnD 不提供炫酷的拖动体验,而是通过帮助我们管理拖拽中的数据变化,再由我们根据这些数据进行渲染。我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用。初次使用可能感觉并不是那么方便,但是如果场景比较复杂,或者是需要高度定制,React DnD 一定是首选。

React DnD 的使用说明可以参见官方文档。本文分析 React DnD 的源码,更深层次的了解这个库。以下的代码来源于 react-dnd 14.0.4。

代码结构

React-DnD 是单个代码仓库,但是打了多个包。这种方式也表示了 React DnD 的三层结构。

___________ ___________ _______________ | | | | | | | | | | | backend-html | | react-dnd | | dnd-core | | | | | | | | backend-touch | |___________| |___________| |_______________| 

react-dnd 是 React 版本的 Drag and Drop 的实现。它定义了 DragSource, DropTarget, DragDropContext 等高阶组件,以及 useDrag,useDrop 等 hook。我们可以简单的理解为这是一个接入层。

dnd-core 是整个拖拽库的核心,它实现了一个和框架无关的拖放管理器,定义了拖放的交互,根据 dnd-core 中定义的规则,我们完全可以根据它自己实现一个 vue-dnd。dnd-core 中使用 redux 做状态管理。

backend 是 React DnD 抽象了后端的概念,这里是 DOM 事件转换为 redux action 的地方。如果是 H5 应用,backend-html,如果是移动端,使用 backend-touch。也支持用户自定义。

DndProvider

如果想要使用 React DnD,首先需要在外层元素上加一个 DndProvider。

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

DndProvider 的本质是一个由 React.createContext 创建一个上下文的容器(组件),用于控制拖拽的行为,数据的共享。DndProvider 的入参是一个 Backend。Backend 是什么呢?React DnD 将 DOM 事件相关的代码独立出来,将拖拽事件转换为 React DnD 内部的 redux action。由于拖拽发生在 H5 的时候是 ondrag,发生在移动设备的时候是由 touch 模拟,React DnD 将这部分单独抽出来,方便后续的扩展,这部分就叫做 Backend。它是 DnD 在 Dom 层的实现。

以下是 DndProvider 的核心代码,通过入参生成一个 manager,这个 manager 用于控制拖拽行为。这个 manager 放到 Provider 中,子节点都可以访问这个 manager。

export const DndProvider: FC> = memo( function DndProvider({ children, ...props }) { const [manager, isGlobalInstance] = getDndContextValue(props) ... return {children} }, ) 

DragDropManager

DndProvider 将 DndProvider 放到了 context 中,这个 manager 非常关键,后续的拖动都依赖于 manager,如下是它的创建过程。

export function createDragDropManager( backendFactory: BackendFactory, globalContext: unknown = undefined, backendOptions: unknown = {}, debugMode = false, ): DragDropManager { const store = makeStoreInstance(debugMode) const monitor = new DragDropMonitorImpl(store, new HandlerRegistryImpl(store)) const manager = new DragDropManagerImpl(store, monitor) const backend = backendFactory(manager, globalContext, backendOptions) manager.receiveBackend(backend) return manager } 

首先看下 store 的创建过程,manager 中 store 的创建使用了 redux 的 createStore 方法,store 是用来以存放应用中所有的 state 的。它的第一个参数 reducer 接收两个参数,分别是当前的 state 树和要处理的 action,返回新的 state 树。

function makeStoreInstance(): Store { return createStore(reduce) } 

manager 中的 store 管理着如下 state,每个 state 都有对应的方法进行更新。

export interface State { dirtyHandlerIds: DirtyHandlerIdsState dragOffset: DragOffsetState refCount: RefCountState dragOperation: DragOperationState stateId: StateIdState } 

标准的 redux 更新数据的方法是 dispatch action 的方式。如下是 dragOffset 更新方法,判断当前 action 的类型,从 payload 中获得需要的参数,然后返回新的 state。

export function reduce( state: State = initialState, action: Action<{ sourceClientOffset: XYCoord clientOffset: XYCoord }>, ): State { const { payload } = action switch (action.type) { case INIT_COORDS: case BEGIN_DRAG: return { initialSourceClientOffset: payload.sourceClientOffset, initialClientOffset: payload.clientOffset, clientOffset: payload.clientOffset, } case HOVER: ... case END_DRAG: case DROP: return initialState default: return state } } 

接下来看 monitor,已知 store 表示的是拖拽过程中的数据,那么我们可以根据这些数据计算出当前的一些状态,比如某个物体是否可以被拖动,某个物体是否正在悬空等等。monitor 提供了一些方法来访问这些数据,不仅如此,monitor 最大的作用是用来监听这些数据的,我们可以为 monitor 添加一些监听器,这样在数据变动之后就能及时响应。

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

export interface DragDropMonitor { 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 就是添加监听函数的方法,其原理是使用了 redux 的 subscribe 方法。

public subscribeToStateChange( listener: Listener, options: { handlerIds: string[] | undefined } = { handlerIds: undefined }, ): Unsubscribe { ... return this.store.subscribe(handleChange) } 

要注意的是,DragDropMonitor 是一个全局的 monitor,它监听的范围是 DndProvider 下所有可拖拽的元素,也就是 monitor 中会存在多个对象,这些拖拽对象有全局唯一性的 ID 标识(从 0 自增的 ID)。这也是 monitor 中的发部分方法都需要传一个 Identifier 的原因。还有一点就是,最好不要存在多个 DndProvider,除非你确定不同 DndProvider 下拖拽元素一定不会交互。

我们在 DndProvider 传入了一个参数 backend,其实它是个工厂方法,执行之后会生成真正的 backend。

manager 比较简单,它包含了之前生成的 monitor, store, backend,还在初始化的时候为 store 添加了一个监听器。它监听 state 中的 refCount 方法, refCount 表示当前标记为可拖拽的对象,如果 refCount 大于 0,初始化 backend,否则,销毁 backend。

export class DragDropManagerImpl implements DragDropManager { private store: Store private monitor: DragDropMonitor private backend: Backend | undefined private isSetUp = false public constructor(store: Store, monitor: DragDropMonitor) { this.store = store this.monitor = monitor store.subscribe(this.handleRefCountChange) } ... private handleRefCountChange = (): void => { const shouldSetUp = this.store.getState().refCount > 0 if (this.backend) { if (shouldSetUp && !this.isSetUp) { this.backend.setup() this.isSetUp = true } else if (!shouldSetUp && this.isSetUp) { this.backend.teardown() this.isSetUp = false } } } } 

manager 创建完成,表示此时我们有了一个 store 来管理拖拽中的数据,有了 monitor 来监听数据和控制行为,能通过 manager 进行注册,可以通过 backend 将 Dom 事件转换为 action。接下来就能使用 useDrag 来创建一个真正的可拖拽对象了。

useDrag

一个元素想要被拖拽,Hooks 的写法如下,使用 useDrag 实现。useDrag 的入参和返回值可以参考官方文档,这里不加赘述。

import { DragPreviewImage, useDrag } from 'react-dnd'; export const Knight: FC = () => { const [{ isDragging }, drag, preview] = useDrag( () => ({ type: ItemTypes.KNIGHT, collect: (monitor) => ({ isDragging: !!monitor.isDragging() }) }), [] ); return ( <>
); };

在 使用 useDrag 的时候,我们配置了入参,是一个函数,这个函数的返回值就是配置参数,useOptionalFactory 就是使用 useMemo 将这个方法包了一层,避免重复调用。

export function useDrag( specArg: FactoryOrInstance< DragSourceHookSpec >, deps?: unknown[], ): [CollectedProps, ConnectDragSource, ConnectDragPreview] { // 获得配置参数 const spec = useOptionalFactory(specArg, deps) // 获得 manager 中的 monitor 的包装对象(DragSourceMonitor) const monitor = useDragSourceMonitor() // 连接 dom 以及 redux const connector = 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 的行为。使其类型变为 DragSourceMonitor。 这就是 useDragSourceMonitor 做的事情,

export function useDragSourceMonitor(): DragSourceMonitor { const manager = useDragDropManager() return useMemo(() => new DragSourceMonitorImpl(manager), [manager]) } 

以上,我们有 Backend 控制 Dom 层级的行为,Store 和 Monitor 控制数据层的变化,那如何让 Monitor 知道现在要监听到底是哪个节点,还需要将这两者连接起来,才能真正的让 Dom 层和数据层保持一致,React DnD 中使用 connector 来连接着两者。

useDragSourceConnector 方法中会 new 一个 SourceConnector 的实例,该实例会接受 backend 作为入参,SourceConnector 实现了 Connector 接口。Connector 中成员变量不多,最重要就是 hooks 对象,该对象用于处理 ref 的逻辑。

export interface Connector { // 获得 ref 指向的 Dom hooks: any // 获得 dragSource connectTarget: any // dragSource 唯一 Id receiveHandlerId(handlerId: Identifier | null): void // 重新连接 dragSource 和 dom reconnect(): void } 

我们在例子中将 ref 属性给到了一个 useDrag 的返回值。该返回值其实就是 hooks 中的 dragSource 方法。

export function useConnectDragSource(connector: SourceConnector) { return useMemo(() => connector.hooks.dragSource(), [connector]) } 

从 dragSource 方法可以看出,connector 中将这个 Dom 节点维护在了 dragSourceNode 属性上。

export class SourceConnector implements Connector { // wrapCon
                
                

-六神源码网