React DnD如何处理拖拽详解

目录
  • 正文
  • 代码结构
  • DndProvider
  • DragDropManager
  • useDrag
  • HTML5Backend
  • TouchBackend
  • 总结

正文

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 backend={HTML5Backend}>
    <TutorialApp />
</DndProvider>

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<DndProviderProps<unknown, unknown>> = memo(
    function DndProvider({ children, ...props }) {
        const [manager, isGlobalInstance] = getDndContextValue(props)
    ...
        return <DndContext.Provider value={manager}>{children}</DndContext.Provider>
    },
)

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<State> {
    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&lt;{
        sourceClientOffset: XYCoord
        clientOffset: XYCoord
    }&gt;,
): 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<State>
    private monitor: DragDropMonitor
    private backend: Backend | undefined
    private isSetUp = false
    public constructor(store: Store<State>, 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 (
        <>
            <DragPreviewImage connect={preview} src={knightImage} />
            <div
                ref={drag}
            >
                ♘
            </div>
        </>
    );
};

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

export function useDrag<DragObject, DropResult, CollectedProps>(
    specArg: FactoryOrInstance<
        DragSourceHookSpec<DragObject, DropResult, CollectedProps>
    >,
    deps?: unknown[],
): [CollectedProps, ConnectDragSource, ConnectDragPreview] {
  // 获得配置参数
    const spec = useOptionalFactory(specArg, deps)
  // 获得 manager 中的 monitor 的包装对象(DragSourceMonitor)
    const monitor = useDragSourceMonitor<DragObject, DropResult>()
    // 连接 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<O, R>(): DragSourceMonitor<O, R> {
    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(() =&gt; connector.hooks.dragSource(), [connector])
}

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

export class SourceConnector implements Connector {
    // wrapConnectorHooks 判断 ref 节点是否是合法的 ReactElement,是的话,执行回调方法
    public hooks = wrapConnectorHooks({
        dragSource: (
            node: Element | ReactElement | Ref<any>,
            options?: DragSourceOptions,
        ) => {
            // dragSourceRef 和 dragSourceNode 赋值 null
            this.clearDragSource()
            this.dragSourceOptions = options || null
            if (isRef(node)) {
                this.dragSourceRef = node as RefObject<any>
            } else {
                this.dragSourceNode = node
            }
            this.reconnectDragSource()
        },
        ...
    })
    ...
}

获得节点后,调用 this.reconnectDragSource(),该方法中,backend 调用 connectDragSource 方法为该节点添加事件监听,后续会分析 backend。

private reconnectDragSource() {
    const dragSource = this.dragSource
    ...
    if (didChange) {
        ...
        this.dragSourceUnsubscribe = this.backend.connectDragSource(
            this.handlerId,
            dragSource,
            this.dragSourceOptions,
        )
    }
}

现在还需要对 Dom 进行抽象,生成唯一ID, 封装为 DragSource 注册到 monitor 上。

export function useRegisteredDragSource<O, R, P>(
    spec: DragSourceHookSpec<O, R, P>,
    monitor: DragSourceMonitor<O, R>,
    connector: SourceConnector,
): void {
    const manager = useDragDropManager()
    // 生成 DragSource
    const handler = useDragSource(spec, monitor, connector)
    const itemType = useDragType(spec)
    // useLayoutEffect
    useIsomorphicLayoutEffect(
        function registerDragSource() {
            if (itemType != null) {
                // DragSource 注册到 monitor
                const [handlerId, unregister] = registerSource(
                    itemType,
                    handler,
                    manager,
                )
                // 更新唯一 ID,触发 reconnect 逻辑
                monitor.receiveHandlerId(handlerId)
                connector.receiveHandlerId(handlerId)
                return unregister
            }
        },
        [manager, monitor, connector, handler, itemType],
    )
}

DragSource 实现以下几个方法,这个几个方法我们使用 useDarg 的时候可以配置同名函数,这些配置的方法会被以下方法调用。

export interface DragSource {
    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 做的事情,首先就是支持一些配置参数,这是最基础的,然后获得 Provider 中的 managre,对其中的一些对象进行包装,屏蔽一些方法,增加一些参数。最重要的就是创建 connector,在界面加载完毕后,connector 通过 ref 的方式获得 Dom 节点的实例,为该节点添加拖拽属性和拖拽事件。同时根据配置参数和 connector 封装 DragSource 对象,将其注册到 monitor 中。

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

HTML5Backend

之前为 DndProvider 注入的参数 HTML5Backend,其实是个工程方法,我们在 DndProvider 除了可以配置 backend 外,还可以配置 backend 的一些参数,当然,backend 的实现不同,传参也不同。DragDropManager 会根据这些参数初始化真正的 backend。

export const HTML5Backend: BackendFactory = function createBackend(
    manager: DragDropManager,
    context?: HTML5BackendContext,
    options?: HTML5BackendOptions,
): HTML5BackendImpl {
    return new HTML5BackendImpl(manager, context, options)
}

如下是 Backend 需要被实现的方法。

export interface Backend {
    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(): Record<string, number>
}

setup 是 backend 的初始化方法,teardown 是 backend 销毁方法。上文提到过,setup 和 teardown 是在 handleRefCountChange 中执行的。React DnD 会在我们第一个使用 useDrag 或是 useDrop 的时候,执行 setup 方法,而在它检测到没有任何地方在使用拖拽功能的时候,执行 teardown 方法。

HTML5BackendImpl 的 setup 方法中执行如下方法,target 默认状态下指的是 window。这里监听了所有的拖拽事件。这是典型的事件委托的方式,统一将拖拽事件的回调函数都绑定在 window 上,不仅能提高性能,而且极大的降低了事件销毁的难度。

private addEventListeners(target: Node) {
    if (!target.addEventListener) {
        return
    }
    target.addEventListener(
        'dragstart',
        this.handleTopDragStart as EventListener,
    )
    target.addEventListener('dragstart', this.handleTopDragStartCapture, true)
    target.addEventListener('dragend', this.handleTopDragEndCapture, true)
    target.addEventListener(
        'dragenter',
        this.handleTopDragEnter as EventListener,
    )
    target.addEventListener(
        'dragenter',
        this.handleTopDragEnterCapture as EventListener,
        true,
    )
    target.addEventListener(
        'dragleave',
        this.handleTopDragLeaveCapture as EventListener,
        true,
    )
    target.addEventListener('dragover', this.handleTopDragOver as EventListener)
    target.addEventListener('dragover', this.handleTopDragOverCapture, true)
    target.addEventListener('drop', this.handleTopDrop as EventListener)
    target.addEventListener(
        'drop',
        this.handleTopDropCapture as EventListener,
        true,
    )
}

HTML5Backend 拖拽的监听函数就是获得拖拽事件的对象,拿到相应的参数。HTML5Backend 通过 Manager 拿到一个 DragDropActions 的实例,执行其中的方法。DragDropActions 本质就是根据参数将其封装为一个 action,最终通过 redux 的 dispatch 将 action 分发,改变 store 中的数据。

export interface DragDropActions {
    beginDrag(
        sourceIds?: Identifier[],
        options?: any,
    ): Action<BeginDragPayload> | undefined
    publishDragSource(): SentinelAction | undefined
    hover(targetIds: Identifier[], options?: any): Action<HoverPayload>
    drop(options?: any): void
    endDrag(): SentinelAction
}

我们看下 connectDragSource 方法。该方法用于将某个 Node 节点转换为可拖拽节点,并且添加监听事件。

HTML5Backend 使用 HTML5 拖放 API 实现。 首先:为了把一个元素设置为可拖放,把 draggable 属性设置为 true。然后监听 ondragstart 事件,该事件在用户开始拖动元素时触发。至于 selectstart,不用关心,是用来处理一些 IE 特殊情况的。

public connectDragSource(
    sourceId: string,
    node: Element,
    options: any,
): Unsubscribe {
    ...
    // 设置 draggable 属性
    node.setAttribute('draggable', 'true')
    // 添加 dragstart 监听
    node.addEventListener('dragstart', handleDragStart)
        // 添加 selectstart 监听
    node.addEventListener('selectstart', handleSelectStart)
    ...
}

Node 上绑定的 dragstart 事件很简单,就是更新了下 sourceId。负责的逻辑绑定在了 window 上。

public handleDragStart(e: DragEvent, sourceId: string): void {
    if (e.defaultPrevented) {
        return
    }
    if (!this.dragStartSourceIds) {
        this.dragStartSourceIds = []
    }
    this.dragStartSourceIds.unshift(sourceId)
}

综上,HTML5Backend 在初始化的时候在 window 上绑定拖拽事件的监听函数,处理拖拽中的坐标数据,状态数据,并将其转换为 action 交由上层的 store 处理。完成由 Dom 事件到数据的转变。元素上绑定的监听只负责更新 sourceId。

TouchBackend

最后简单的看下 TouchBackend,与 HTML5Backend 相比,TouchBackend 的使用场景更加广泛,因为它不依赖于 H5 的 API,兼容性很好,既能用于浏览器端,又能用在移动端。

TouchBackend 使用简单的事件来模拟拖放行为。比如在浏览器端,使用的是 mousedown,mousemove,mouseup。移动端使用 touchstart,touchmove,touchend。

const eventNames: Record&lt;ListenerType, EventName&gt; = {
    [ListenerType.mouse]: {
        start: 'mousedown',
        move: 'mousemove',
        end: 'mouseup',
        contextmenu: 'contextmenu',
    },
    [ListenerType.touch]: {
        start: 'touchstart',
        move: 'touchmove',
        end: 'touchend',
    },
    [ListenerType.keyboard]: {
        keydown: 'keydown',
    },
}

总结

本文分析了 React-DnD 是如何处理拖拽这一行为的。

首先在设计上,React-DnD 使用了分层设计的方式,react-dnd 是接入层,它为准备了高阶组件和 Hooks 两种方式。dnd-core 是核心,它定义了拖拽接口,管理方式,数据流向。backend 中将 DOM 事件转换为 redux action 的地方,该层用于屏蔽设备之间的差异性。

dnd-core 使用了 redux 管理数据,这些数据通过 dispatch action 进行修改,使用 monitor 进行数据的监控,使用 connector 连接 dom 和 store。最终拖拽实现依赖于 backend,为节点添加了监听事件,然后将事件转化为 action。

整体上看,React-DnD 的核心思路就是将事件转换为数据,设计上参考了 redux 的单一数据流的方式(毕竟一个作者写的),这样我们在处理拖拽的时候就可以关注于数据方面的变化,而不用费心去维护拖拽中的一些中间状态,更不用自己去添加,移除事件,是非常好的一种设计。

以上就是React DnD如何处理拖拽详解的详细内容,更多关于React DnD 拖拽处理的资料请关注我们其它相关文章!

(0)

相关推荐

  • react+react-beautiful-dnd实现代办事项思路详解

    目录 react+react-beautiful-dnd应用 效果预览 实现思路 index.js入口文件配置 app.jsx主页面配置 untils/with-context.js封装工具todoContext components/TodoHeader.jsx页面头部 components/TodoInput.jsx该文件主要负责添加事件 格式DragDropContext最外面盒子Droppable第二层盒子 格式Draggable最里面盒子 components/TodoList.jsx

  • react-beautiful-dnd 实现组件拖拽

    一个React.js 的 漂亮,可移植性 列表拖拽库.想了解更多react-beautiful-dnd特点适用人群请看官方文档.中文翻译文档 npm:https://www.npmjs.com/package/react-beautiful-dnd 1.安装 ​ 在已有react项目中 执行以下命令 so easy. # yarn yarn add react-beautiful-dnd # npm npm install react-beautiful-dnd --save 2.APi 详情查

  • react-dnd实现任意拖动与互换位置

    本文实例为大家分享了react-dnd实现任意拖动与互换位置的具体代码,供大家参考,具体内容如下 react-dnd用法 hooks组件 1.使用DndProvider定义一个可以拖拽的范围 import { HTML5Backend } from 'react-dnd-html5-backend'; import { DndProvider } from 'react-dnd'; class App extends Component{   render () {     return (  

  • react-dnd API拖拽工具详细用法示例

    目录 前言 概念 核心API DndProvider Backend useDrag useDrag返回三个参数 useDrag传入两个参数 DragSourceMonitor对象 useDrop useDrag返回两个参数 useDrop传入一个参数 DropTargetMonitor对象 数据流转 拖拽预览 DragPreviewImage useDragLayer 其他使用场景 批量拖拽 拖拽排序 最后 前言 最近公司准备开发一个审批流系统,其中会用到拖拽工具来搭建流程,关于拖拽的实现我们

  • 使用react-beautiful-dnd实现列表间拖拽踩坑

    为什么选用react-beautiful-dnd 相比于react-dnd,react-beautiful-dnd更适用于列表之间拖拽的场景,支持移动端,且较为容易上手. 基本使用方法 基本概念 DragDropContext:构建一个可以拖拽的范围 onDragStart:拖拽开始回调 onDragUpdate:拖拽中的回调 onDragEnd:拖拽结束时的回调 Droppable - 可以放置拖拽块的区域 Draggalbe - 可被拖拽的元素 使用方法 把你想能够拖放的代码放到DragDr

  • React DnD如何处理拖拽详解

    目录 正文 代码结构 DndProvider DragDropManager useDrag HTML5Backend TouchBackend 总结 正文 React DnD 是一个专注于数据变更的 React 拖拽库,通俗的将,你拖拽改变的不是页面视图,而是数据.React DnD 不提供炫酷的拖动体验,而是通过帮助我们管理拖拽中的数据变化,再由我们根据这些数据进行渲染.我们可能需要写额外的视图层来完成想要的效果,但是这种拖拽管理方式非常的通用,可以在任何场景下使用.初次使用可能感觉并不是那

  • React实现卡片拖拽效果流程详解

    前提摘要: 学习宋一玮 React 新版本 + 函数组件 &Hooks 优先 开篇就是函数组件+Hooks 实现的效果如下: 学到第11篇了 照葫芦画瓢,不过老师在讲解的过程中没有考虑拖拽目标项边界问题,我稍微处理了下这样就实现拖拽流畅了 下面就是主要的代码了,实现拖拽(src/App.js): 核心在于标记当前项,来源项,目标项,并且在拖拽完成时对数据处理,更新每一组数据(useState): /** @jsxImportSource @emotion/react */ // 上面代码是使用e

  • 基于react组件之间的参数传递(详解)

    1.父组件向子组件传递参数 class Child extends Component { componentDidMount(){ let name = this.props.default; console,log(name); } render(){ const { default} = this.props; return ( <Input /> ) } } import React, { Component } from 'react'; import Child from './C

  • js中自定义react数据验证组件实例详解

    我们在做前端表单提交时,经常会遇到要对表单中的数据进行校验的问题.如果用户提交的数据不合法,例如格式不正确.非数字类型.超过最大长度.是否必填项.最大值和最小值等等,我们需要在相应的地方给出提示信息.如果用户修正了数据,我们还要将提示信息隐藏起来. 有一些现成的插件可以让你非常方便地实现这一功能,如果你使用的是knockout框架,那么你可以借助于Knockout-Validation这一插件.使用起来很简单,例如我下面的这一段代码: ko.validation.locale('zh-CN');

  • react的hooks的用法详解

    hooks的作用 它改变了原始的React类的开发方式,改用了函数形式;它改变了复杂的状态操作形式,让程序员用起来更轻松;它改变了一个状态组件的复用性,让组件的复用性大大增加. useState // 声明状态 const [ count , setCount ] = useState(0); // 使用状态 <p>You clicked {count} times</p> <button onClick={()=>{setCount(count+1)}}>cli

  • React Fragment介绍与使用详解

    目录 前言 Fragments出现动机 React Fragment介绍与使用 <React.Fragment> 与 <>区别 前言 在向 DOM 树批量添加元素时,一个好的实践是创建一个document.createDocumentFragment,先将元素批量添加到 DocumentFragment 上,再把 DocumentFragment 添加到 DOM 树,减少了 DOM操作次数的同时也不会创建一个新元素. 和 DocumentFragment 类似,React 也存在

  • React团队测试并发特性详解

    目录 引言 遇到的困境 1. 如何表达渲染结果? 2. 如何测试并发环境? React的应对策略 实现一个渲染器 如何测试并发环境? 总结 引言 React18进入大家视野已经有一段时间了,不知道各位有没有尝试并发特性呢? 当启用并发特性后,React会从同步更新变为异步.带优先级.可中断的更新. 这也为编写单元测试带来了一些难度. 本文来聊聊React团队如何测试并发特性. 遇到的困境 主要有两个问题需要面对. 1. 如何表达渲染结果? React可以对接不同宿主环境的渲染器,大家最熟悉的渲染

  • react实现自定义拖拽hook

    前沿 最近发现公司的产品好几个模块用到了拖拽功能,之前拖拽组件是通过Html5 drag Api 实现的但体验并不是很好,顺便将原来的拖拽组建稍做修改,写一个自定义hook,方便大家使用拖拽功能. 正文 拖拽功能原理: 1.拖拽元素通过addEventListener监听器添加鼠标按下,鼠标移动,以及鼠标抬起事件.2.再通过getBoundingClientRect() 得到拖拽元素四周相对于可拖拽区域边界的距离.3.鼠标移动时计算x轴和y轴的移动偏移量.4.通过element.style.tr

  • React中父子组件通信详解

    目录 父组件向子组件通信 存在期 父组件向子组件通信 在父组件中,为子组件添加属性数据,即可实现父组件向子组件通信.传递的数据可以分成两类 子组件是作为属性来接收这些数据的 第一类就是数据:变量,对象,属性数据,状态数据等等 这些数据发生改变,子组件接收的属性数据就发生了改变. 第二类就是方法:父组件可以向子组件传递属性方法,子组件接收方法,并可以在组件内执行,有两种执行方式 注意:父组件传给子组件的方法是不能执行的,执行了相当于将方法的返回值传递给子组件. 第一种 作为事件回调函数执行 参数默

  • React中react-redux和路由详解

    目录 观察者模式解决组件间通信问题 react-redux connect方法 Provider组件 路由 使用路由 默认路由 路由重定向 获取路由参数 路由导航 观察者模式解决组件间通信问题 使用观察者解决组件间通信,分成两步 在一个组件中,订阅消息 在另一个组件中,发布消息 发布消息之后,订阅的消息回调函数会执行,在函数中,我们修改状态,这样就可以实现组件间通信了.这就是reflux框架的实现. react-redux redux早期被设计成可以在各个框架中使用,因此在不同的框架中使用的时候

随机推荐