1.拖拽应用场景 #

2.React DnD #

3.核心知识 #

3.1 HTML拖放API #

事件 触发时刻
dragstart 当用户开始拖拽一个元素时触发
dragover 当元素被拖到一个可释放目标上时触发(每100毫秒触发一次)
dragend 当拖拽操作结束时触发

3.2 触摸事件 #

事件 触发时刻
touchstart 触摸开始
touchmove 接触点移动
touchend 触摸结束

3.3 getBoundingClientRect #

3.4 clientX/Y #

3.5 React Hooks #

3.6 高阶组件 #

const NewComponent = higherOrderComponent(OldComponent)

4. 核心概念 #

reactdnd

4.1 DndProvider #

4.2 backend(后端) #

4.3 DragDropManager(管理器)和Registry(注册器) #

4.4 Items and Types(项目和类型) #

kanbanben_1634867988749

4.5 Monitors(监听器) #

4.5.1 DragSourceMonitor(拖动源监听器) #

4.5.2 DropTargetMonitor(放置目标监听器) #

4.6 Connectors(连接器) #

4.6.1 DragSourceConnector(拖动源连接器) #

4.6.2 DragTargetConnector(放置目标连接器) #

4.7 DragSources(拖动源)和DropTargets(放置目标) #

4.7.1 DragSource #

4.7.2 DropTarget #

4.8 useDrag #

4.9 useDrop #

5. 拖拽排序实战 #

5.1 安装 #

npm install react-dnd react-dnd-html5-backend --save

5.2 绘制容器 #

5.2.1 src\index.js #

src\index.js

import React from 'react';
import ReactDOM from 'react-dom';
import Container from './Container';
import { DndProvider } from 'react-dnd';
import { HTML5Backend } from 'react-dnd-html5-backend';

ReactDOM.render(
  <DndProvider backend={HTML5Backend}>
    <Container />
  </DndProvider>,
  document.getElementById('root')
);

5.2.2 Container.js #

src\Container.js

import React from 'react'
const style = {
    width: '300px'
}
function Container() {
    return (
        <div style={style}>
            Container
        </div>
    )
}

export default Container;

5.3 绘制卡片 #

5.3.1 src\Card.js #

src\Card.js

import React from 'react'
const style = {
    backgroundColor: 'red',
    padding: '5px',
    margin: '5px',
    border: '1px dashed gray',
    cursor: 'move'
}
export default function Card({ text }) {
    return (
        <div style={style}>
            {text}
        </div>
    )
}

5.3.2 src\Container.js #

+import React, { useState } from 'react'
+import Card from './Card';
const style = {
    width: '300px'
}
function Container() {
+   const [cards, setCards] = useState([
+       { id: 'card1', text: '卡片1' },
+       { id: 'card2', text: '卡片2' },
+       { id: 'card3', text: '卡片3' }
+   ]);
    return (
        <div style={style}>
+           {
+               cards.map((card, index) => (
+                   <Card key={card.id} text={card.text} />
+               ))
+           }
        </div>
    )
}
export default Container;

5.4 拖动卡片 #

5.4.1 ItemTypes.js #

src\ItemTypes.js

export const CARD = 'card';
/**
 * React DnD 使用数据而不是视图作为来源
 * 当您在屏幕上拖动某物时,我们并不是说正在拖动组件或 DOM 节点。 相反,我们说某种类型的项目正在被拖动
 * 什么是项目? 一个项目是一个简单的 JavaScript 对象,描述被拖动的内容
 * 那什么是type(类型)呢? 类型是一个字符串(或Symbol),它唯一地标识了应用程序中的项目。 在看板应用程序中,您可能有一个代表可拖动卡片的卡片类型
 * 类型很有用,因为随着您的应用程序的增长,您可能希望使更多内容可拖动,但您不一定希望所有现有的放置目标突然开始对新项目做出反应
 * 这些类型允许您指定兼容的拖放源和放置目标
 */

5.4.2 Container.js #

src\Container.js

import React, { useState } from 'react'
import Card from './Card';
const style = {
    width: '200px'
}
function Container() {
    const [cards, setCards] = useState([
        { id: 'card1', text: '卡片1' },
        { id: 'card2', text: '卡片2' },
        { id: 'card3', text: '卡片3' }
    ]);
    return (
        <div style={style}>
            {
                cards.map((card, index) => (
+                   <Card key={card.id} text={card.text} id={card.id} index={index} />
                ))
            }
        </div>
    )
}

export default Container;

5.4.3 Card.js #

src\Card.js

import React, { useRef } from 'react'
+import { useDrag } from 'react-dnd';
+import { CARD } from './ItemTypes';
const style = {
    backgroundColor: 'red',
    padding: '5px',
    margin: '5px',
    border: '1px dashed gray',
    cursor: 'move'
}
export default function Card({ text, id, index }) {
+   const ref = useRef(null);
+   //useDrag hook 提供了一种将组件作为拖动源连接到 DnD 系统的方法
+   //Collected Props: 包含从 collect 函数收集的属性的对象。 如果没有定义 collect 函数,则返回一个空对象
+   //DragSource Ref: 拖动源的连接器功能。 这必须附加到 DOM 的可拖动部分
+   const [{ isDragging }, drag] = useDrag({//spec
+       //必需的。 这必须是字符串或Symbol。 只有为相同类型注册的放置目标才会对此项目做出反应
+       type: CARD,
+       //item: 必需的 (对象或者函数) 当这是一个对象时,它是一个描述被拖动数据的普通 JavaScript 对象
+       item: () => ({ id, index }),
+       //collect:  收集功能。 它应该返回一个普通的属性对象,以返回以注入到您的组件属性中
+       //它接收两个参数,monitor 和 props
+       collect: (monitor) => ({//要收集的属性
+           isDragging: monitor.isDragging()
+       })
+   });
+   const opacity = isDragging ? 0.1 : 1;
+   drag(ref)
    return (
+       <div ref={ref} style={{ ...style, opacity }}>
            {text}
        </div>
    )
}

5.5 放置卡片 #

5.5.1 Container.js #

src\Container.js

import React, { useState } from 'react'
import Card from './Card';
const style = {
    width: '200px'
}
function Container() {
    const [cards, setCards] = useState([
        { id: 'card1', text: '卡片1' },
        { id: 'card2', text: '卡片2' },
        { id: 'card3', text: '卡片3' }
    ]);
+   const moveCard = (dragIndex, hoverIndex) => {
+       const dragCard = cards[dragIndex];
+       const cloneCards = [...cards];
+       cloneCards.splice(dragIndex, 1);
+       cloneCards.splice(hoverIndex, 0, dragCard);
+       setCards(cloneCards);
+   }
    return (
        <div style={style}>
            {
                cards.map((card, index) => (
+                   <Card key={card.id} text={card.text} id={card.id} index={index} moveCard={moveCard} />
                ))
            }
        </div>
    )
}
export default Container;

5.5.2 src\Card.js #

src\Card.js

import React, { useRef } from 'react'
+import { useDrag, useDrop } from 'react-dnd';
import { CARD } from './ItemTypes';
const style = {
    backgroundColor: 'red',
    padding: '5px',
    margin: '5px',
    border: '1px dashed gray',
    cursor: 'move'
}
export default function Card({ text, id, index, moveCard }) {
    const ref = useRef(null);
+   //useDrop hook 为您提供了一种将组件连接到 DnD 系统作为放置目标的方法
+   // Collected Props: 包含从 collect 函数收集的属性的对象
+   //DropTarget Ref: 放置目标的连接器函数。 这必须附加到 DOM 的放置目标部分
+   const [, drop] = useDrop({
+       //一个字符串或一个Symbol,这个放置目标只会对指定类型的拖拽源产生的项目做出反应
+       accept: CARD,
+       //收集功能。 它应该返回一个普通的属性对象,以返回以注入到您的组件属性中
+       collect: (monitor) => ({}),
+       //当在组件发生hover事件时调用
+       hover(item, monitor) {
+           //被拖动卡片的索引
+           const dragIndex = item.index;
+           //hover卡片的索引
+           const hoverIndex = index;
+           //如果一样什么都不做
+           if (dragIndex === hoverIndex) {
+               return;
+           }
+           //获取hover卡片的位置信息
+           const { top, bottom } = ref.current.getBoundingClientRect();
+           //获取hover卡片高度的一半
+           const halfOfHoverHeight = (bottom - top) / 2;
+           //获取鼠标最新的X和Y坐标
+           const { y } = monitor.getClientOffset();
+           const hoverClientY = y - top;
+           if ((dragIndex < hoverIndex && hoverClientY > halfOfHoverHeight)
+               || (dragIndex > hoverIndex && hoverClientY < halfOfHoverHeight)) {
+               moveCard(dragIndex, hoverIndex);
+               item.index = hoverIndex;
+           }
+       }
+   });
    //useDrag hook 提供了一种将组件作为拖动源连接到 DnD 系统的方法
    //Collected Props: 包含从 collect 函数收集的属性的对象。 如果没有定义 collect 函数,则返回一个空对象
    //DragSource Ref: 拖动源的连接器功能。 这必须附加到 DOM 的可拖动部分
    const [{ isDragging }, drag] = useDrag({//spec
        //必需的。 这必须是字符串或Symbol。 只有为相同类型注册的放置目标才会对此项目做出反应
        type: CARD,
        //item: 必需的 (对象或者函数) 当这是一个对象时,它是一个描述被拖动数据的普通 JavaScript 对象
        item: () => ({ id, index }),
        //collect:  收集功能。 它应该返回一个普通的属性对象,以返回以注入到您的组件属性中
        //它接收两个参数,monitor 和 props
        collect: (monitor) => ({//要收集的属性
            isDragging: monitor.isDragging()
        })
    });
    const opacity = isDragging ? 0.1 : 1;
    drag(ref)
+   drop(ref)
    return (
        <div ref={ref} style={{ ...style, opacity }}>
            {text}
        </div>
    )
}

5.实现Provider #

5.1 react-dnd\index.js #

src\react-dnd\index.js

export * from './core'

5.2 core\index.js #

src\react-dnd\core\index.js

export { default as DndContext } from './DndContext';
export { default as DndProvider } from './DndProvider';

5.3 DndContext.js #

src\react-dnd\core\DndContext.js

import React from 'react';
const DndContext = React.createContext({});
export default DndContext;

5.4 DndProvider.js #

src\react-dnd\core\DndProvider.js

import DndContext from './DndContext';
import { createDragDropManager } from '../../dnd-core';
function DndProvider({ backend, children }) {
    let value = { dragDropManager: createDragDropManager(backend) };
    return (
        <DndContext.Provider value={value}>
            {children}
        </DndContext.Provider>
    )
}
export default DndProvider;

5.5 dnd-core\index.js #

src\dnd-core\index.js

export { default as createDragDropManager } from './createDragDropManager';

5.6 createDragDropManager.js #

src\dnd-core\createDragDropManager.js

import { createStore } from 'redux';
import reducer from './reducers';
import DragDropManagerImpl from './classes/DragDropManagerImpl';
function createDragDropManager(backendFactory) {
    //创建redux仓库
    const store = createStore(reducer);
    const manager = new DragDropManagerImpl(store);
    const backend = backendFactory(manager);
    manager.receiveBackend(backend);
    return manager;
}
export default createDragDropManager;

5.7 DragDropManagerImpl.js #

src\dnd-core\classes\DragDropManagerImpl.js

class DragDropManagerImpl {
    store;
    backend;
    constructor(store) {
        this.store = store;
    }
    receiveBackend(backend) {
        this.backend = backend;
    }
    getBackend() {
        return this.backend;
    }
}
export default DragDropManagerImpl;

5.8 reducers\index.js #

src\dnd-core\reducers\index.js

function reducer(state = {}, action) {
    return {};
}
export default reducer;

5.9 react-dnd-html5-backend\index.js #

src\react-dnd-html5-backend\index.js

import HTML5BackendImpl from './HTML5BackendImpl';
export function HTML5Backend(manager) {
    return new HTML5BackendImpl(manager);
}

5.10 HTML5BackendImpl.js #

src\react-dnd-html5-backend\HTML5BackendImpl.js

class HTML5BackendImpl {}
export default HTML5BackendImpl;

6.实现Monitor #

6.1 react-dnd\index.js #

src\react-dnd\index.js

export * from './core'
+export * from './hooks';

6.2 hooks\index.js #

src\react-dnd\hooks\index.js

export { default as useDrag } from './useDrag';

6.3 useDrag\index.js #

src\react-dnd\hooks\useDrag\index.js

import useDragSourceMonitor from './useDragSourceMonitor';

function useDrag(spec) {
    const monitor = useDragSourceMonitor();
    console.log(monitor);
    return [{}, () => { }];
}
export default useDrag;

6.4 useDragSourceMonitor.js #

src\react-dnd\hooks\useDrag\useDragSourceMonitor.js

import { useMemo } from 'react';
import useDragDropManager from '../useDragDropManager';
import { DragSourceMonitorImpl } from '../../internals'
function useDragSourceMonitor() {
    const manager = useDragDropManager();
    return useMemo(() => new DragSourceMonitorImpl(manager), [manager]);
}
export default useDragSourceMonitor;

6.5 useDragDropManager.js #

src\react-dnd\hooks\useDragDropManager.js

import { useContext } from 'react';
import { DndContext } from '../core';
function useDragDropManager() {
    const { dragDropManager } = useContext(DndContext);
    return dragDropManager;
}
export default useDragDropManager;

6.6 internals\index.js #

src\react-dnd\internals\index.js

export { default as DragSourceMonitorImpl } from './DragSourceMonitorImpl';

6.7 DragSourceMonitorImpl.js #

src\react-dnd\internals\DragSourceMonitorImpl.js

class DragSourceMonitorImpl {
    internalMonitor
    constructor(manager) {
        this.internalMonitor = manager.getGlobalMonitor();
    }
}
export default DragSourceMonitorImpl;

6.8 dnd-core\index.js #

src\dnd-core\index.js

export { default as createDragDropManager } from './createDragDropManager';
export { default as DragDropManagerImpl } from './classes/DragDropManagerImpl';
+export { default as DragDropMonitorImpl } from './classes/DragDropMonitorImpl';

6.9 createDragDropManager.js #

src\dnd-core\createDragDropManager.js

import { createStore } from 'redux';
import reducer from './reducers';
import DragDropManagerImpl from './classes/DragDropManagerImpl';
+import DragDropMonitorImpl from './classes/DragDropMonitorImpl';
function createDragDropManager(backendFactory) {
    //创建redux仓库
    const store = createStore(reducer);
+   const globalMonitor = new DragDropMonitorImpl(store);
+   const manager = new DragDropManagerImpl(store, globalMonitor);
    const backend = backendFactory(manager);
    manager.receiveBackend(backend);
    return manager;
}
export default createDragDropManager;

6.10 DragDropMonitorImpl.js #

src\dnd-core\classes\DragDropMonitorImpl.js

class DragDropMonitorImpl {
    store
    constructor(store) {
        this.store = store
    }
}
export default DragDropMonitorImpl;

6.11 DragDropManagerImpl.js #

src\dnd-core\classes\DragDropManagerImpl.js

class DragDropManagerImpl {
    store;
    backend;
+   globalMonitor;
+   constructor(store, globalMonitor) {
        this.store = store;
+       this.globalMonitor = globalMonitor;
    }
    receiveBackend(backend) {
        this.backend = backend;
    }
    getBackend() {
        return this.backend;
    }
+   getGlobalMonitor() {
+        return this.globalMonitor;
+   }
}
export default DragDropManagerImpl;

7.实现Registry #

7.1 HandlerRegistryImpl.js #

src\dnd-core\classes\HandlerRegistryImpl.js

class HandlerRegistryImpl {
    store;//redux仓库
    constructor(store) {
        this.store = store;
    }
}
export default HandlerRegistryImpl;

7.2 createDragDropManager.js #

src\dnd-core\createDragDropManager.js

import { createStore } from 'redux';
import reducer from './reducers';
+import HandlerRegistryImpl from './classes/HandlerRegistryImpl'
import DragDropManagerImpl from './classes/DragDropManagerImpl';
import DragDropMonitorImpl from './classes/DragDropMonitorImpl';
function createDragDropManager(backendFactory) {
    //创建redux仓库
    const store = createStore(reducer);
+   const registry = new HandlerRegistryImpl(store);
+   const globalMonitor = new DragDropMonitorImpl(store, registry);
    const manager = new DragDropManagerImpl(store, globalMonitor);
    const backend = backendFactory(manager);
    manager.receiveBackend(backend);
    return manager;
}
export default createDragDropManager;

7.3 DragDropMonitorImpl.js #

src\dnd-core\classes\DragDropMonitorImpl.js

class DragDropMonitorImpl {
    store
+   registry
    constructor(store, registry) {
        this.store = store
+       this.registry = registry;
    }
}
export default DragDropMonitorImpl;

8.useDragSourceConnector #

8.1 useDrag\index.js #

src\react-dnd\hooks\useDrag\index.js

import useDragSourceMonitor from './useDragSourceMonitor';
+import useDragSourceConnector from './useDragSourceConnector';
function useDrag(spec) {
    const monitor = useDragSourceMonitor();
+   const connector = useDragSourceConnector();
+   console.log(connector);
    return [{}, () => { }];
}
export default useDrag;

8.2 useDragSourceConnector.js #

src\react-dnd\hooks\useDrag\useDragSourceConnector.js

import { useMemo } from 'react';
import useDragDropManager from '../useDragDropManager';
import { SourceConnector } from '../../internals';
function useDragSourceConnector() {
    const manager = useDragDropManager();
    const connector = useMemo(() => new SourceConnector(manager.getBackend()), [manager]);
    return connector;
}
export default useDragSourceConnector;

8.3 internals\index.js #

src\react-dnd\internals\index.js

export { default as DragSourceMonitorImpl } from './DragSourceMonitorImpl';
+export { default as SourceConnector } from './SourceConnector';

8.4 SourceConnector.js #

src\react-dnd\internals\SourceConnector.js

class SourceConnector {
    backend
    constructor(backend) {
        this.backend = backend;
    }
    connect() {
        console.log('连接React和DOM');
    }
}

export default SourceConnector;

9.创建DragSource和DragType #

9.1 src\react-dnd\hooks\useDrag\index.js #

src\react-dnd\hooks\useDrag\index.js

import useDragSourceMonitor from './useDragSourceMonitor';
import useDragSourceConnector from './useDragSourceConnector';
+import useRegisteredDragSource from './useRegisteredDragSource';
function useDrag(spec) {
//  const monitor = useDragSourceMonitor();
    const connector = useDragSourceConnector();
+   useRegisteredDragSource(spec, monitor, connector);
    return [{}, () => { }];
}
export default useDrag;

9.2 useRegisteredDragSource.js #

src\react-dnd\hooks\useDrag\useRegisteredDragSource.js

import useDragDropManager from '../useDragDropManager';
import useDragSource from './useDragSource';
import { useDragType } from './useDragType';
function useRegisteredDragSource(spec, monitor, connector) {
    const manager = useDragDropManager();
    const handler = useDragSource(spec, monitor, connector);
    console.log('handler', handler);
    const itemType = useDragType(spec);
    console.log(itemType);
}
export default useRegisteredDragSource;

9.3 useDragSource.js #

src\react-dnd\hooks\useDrag\useDragSource.js

/* eslint-disable react-hooks/exhaustive-deps */
import { useMemo, useEffect } from 'react';
import { DragSourceImpl } from './DragSourceImpl';
function useDragSource(spec, monitor, connector) {
    const handler = useMemo(() => {
        return new DragSourceImpl(spec, monitor, connector);
    }, [monitor, connector]);
    useEffect(() => {
        handler.spec = spec;
    }, [handler, spec]);
    return handler;
}
export default useDragSource;

9.4 DragSourceImpl.js #

src\react-dnd\hooks\useDrag\DragSourceImpl.js

export class DragSourceImpl {
    spec;
    monitor;
    connector;
    constructor(spec, monitor, connector) {
        this.spec = spec;
        this.monitor = monitor;
        this.connector = connector;
    }
}

9.5 useDragType.js #

src\react-dnd\hooks\useDrag\useDragType.js

import { useMemo } from 'react';
export function useDragType(spec) {
    return useMemo(() => spec.type, [spec]);
}

10.registerSource #

10.1 useRegisteredDragSource.js #

src\react-dnd\hooks\useDrag\useRegisteredDragSource.js

+import { useLayoutEffect } from 'react';
import useDragDropManager from '../useDragDropManager';
import useDragSource from './useDragSource';
import { useDragType } from './useDragType';
+import { registerSource } from '../../internals';
function useRegisteredDragSource(spec, monitor, connector) {
    const manager = useDragDropManager();
    const handler = useDragSource(spec, monitor, connector);
    const itemType = useDragType(spec);
+   useLayoutEffect(function () {
+       const handlerId = registerSource(itemType, handler, manager);
+       monitor.receiveHandlerId(handlerId);
+       connector.receiveHandlerId(handlerId);
+       console.log('dragSources', manager.globalMonitor.registry.dragSources);
+       console.log('types', manager.globalMonitor.registry.types);
+   }, [connector, handler, itemType, manager, monitor]);
}
export default useRegisteredDragSource;

10.2 DragSourceMonitorImpl.js #

src\react-dnd\internals\DragSourceMonitorImpl.js

class DragSourceMonitorImpl {
    internalMonitor
+   handlerId
    constructor(manager) {
        this.internalMonitor = manager.getGlobalMonitor();
    }
+   receiveHandlerId(handlerId) {
+       this.handlerId = handlerId;
+   }
}
export default DragSourceMonitorImpl;

10.3 SourceConnector.js #

src\react-dnd\internals\SourceConnector.js

class SourceConnector {
    backend
+   handlerId
    constructor(backend) {
        this.backend = backend;
    }
    connect() {
        console.log('连接React和DOM');
    }
+   receiveHandlerId(handlerId) {
+       this.handlerId = handlerId;
+   }
}

export default SourceConnector;

10.4 registerSource.js #

src\react-dnd\internals\registerSource.js

function registerSource(type, handler, manager) {
    const registry = manager.getRegistry();
    const handlerId = registry.addSource(type, handler);
    return handlerId;
}
export default registerSource;

10.5 internals\index.js #

src\react-dnd\internals\index.js

export { default as DragSourceMonitorImpl } from './DragSourceMonitorImpl';
export { default as SourceConnector } from './SourceConnector';
+export { default as registerSource } from './registerSource';

10.6 DragDropManagerImpl.js #

src\dnd-core\classes\DragDropManagerImpl.js

class DragDropManagerImpl {
    store;
    backend;
    globalMonitor;
    constructor(store, globalMonitor) {
        this.store = store;
        this.globalMonitor = globalMonitor;
    }
    receiveBackend(backend) {
        this.backend = backend;
    }
    getBackend() {
        return this.backend;
    }
    getGlobalMonitor() {
        return this.globalMonitor;
    }
+   getRegistry() {
+       return this.globalMonitor.registry;
+   }
}
export default DragDropManagerImpl;

10.7 HandlerRegistryImpl.js #

src\dnd-core\classes\HandlerRegistryImpl.js

+import { HandlerRole } from '../interfaces';
+import { getNextUniqueId } from '../utils';
+import { addSource } from '../actions/registry';
class HandlerRegistryImpl {
    store;//redux仓库
+   types = new Map(); //不同handlerId的类型
+   dragSources = new Map();//拖放来源
    constructor(store) {
        this.store = store;
    }
+   addSource(type, source) {
+       const handlerId = this.addHandler(HandlerRole.SOURCE, type, source);
+       this.store.dispatch(addSource(handlerId));
+       return handlerId;
+   }
+   addHandler(role, type, handler) {
+       const handlerId = getNextHandlerId(role);//获取下一个handleId
+       this.types.set(handlerId, type);
+       if (role === HandlerRole.SOURCE) {
+           this.dragSources.set(handlerId, handler);
+       }
+       return handlerId;
+   }
+   getSource(handlerId) {
+       return this.dragSources.get(handlerId);
+   }
+   getSourceType(handlerId) {
+       return this.types.get(handlerId);
+   }
}
+function getNextHandlerId(role) {
+    const id = getNextUniqueId().toString();
+    switch (role) {
+        case HandlerRole.SOURCE:
+            return `S${id}`;
+        case HandlerRole.TARGET:
+            return `T${id}`;
+        default:
+            throw new Error(`Unknown Handler Role: ${role}`);
+    }
+}
export default HandlerRegistryImpl;

10.8 interfaces.js #

src\dnd-core\interfaces.js

export var HandlerRole = {
    "SOURCE": "SOURCE",
    "TARGET": "TARGET"
}

10.9 getNextUniqueId.js #

src\dnd-core\utils\getNextUniqueId.js

let nextUniqueId = 0;
function getNextUniqueId() {
    return nextUniqueId++;
}
export default getNextUniqueId;

10.10 utils\index.js #

src\dnd-core\utils\index.js

export { default as getNextUniqueId } from './getNextUniqueId';

10.11 registry.js #

src\dnd-core\actions\registry.js

export const ADD_SOURCE = 'dnd-core/ADD_SOURCE';
export function addSource(handlerId) {
    return {
        type: ADD_SOURCE,
        payload: { handlerId }
    };
}

11.connectDragSource #

11.1 useDrag\index.js #

src\react-dnd\hooks\useDrag\index.js

import useDragSourceMonitor from './useDragSourceMonitor';
import useDragSourceConnector from './useDragSourceConnector';
import useRegisteredDragSource from './useRegisteredDragSource';
+import useConnectDragSource from './useConnectDragSource';
function useDrag(spec) {
    const monitor = useDragSourceMonitor();
    const connector = useDragSourceConnector();
    useRegisteredDragSource(spec, monitor, connector);
    return [
        {},
+       useConnectDragSource(connector)
    ];
}
export default useDrag;

11.2 useConnectDragSource.js #

src\react-dnd\hooks\useDrag\useConnectDragSource.js

import { useMemo } from 'react';
function useConnectDragSource(connector) {
    return useMemo(() => connector.receiveDragSource, [connector]);
}
export default useConnectDragSource

11.3 SourceConnector.js #

src\react-dnd\internals\SourceConnector.js

class SourceConnector {
    backend
    handlerId  //拖动源唯一标识
+   dragSourceRef //DOM节点
    constructor(backend) {
        this.backend = backend;
    }
+   get dragSource() {
+       return this.dragSourceRef && this.dragSourceRef.current;
+   }
+   connect() {
+       if (!this.handlerId || !this.dragSource) {
+           return;
+       }
+       this.backend.connectDragSource(this.handlerId, this.dragSource);
+   }

    //useRegisteredDragSource中useLayoutEffect赋值
    receiveHandlerId(handlerId) {
        this.handlerId = handlerId;
+       this.connect();
    }
+   receiveDragSource = (dragSourceRef) => {
+       this.dragSourceRef = dragSourceRef;
+   }
}

export default SourceConnector;

11.4 HTML5BackendImpl.js #

src\react-dnd-html5-backend\HTML5BackendImpl.js

class HTML5BackendImpl {
+   dragStartSourceId
+   connectDragSource(handleId, node) {
+       node.setAttribute('draggable', 'true');
+       node.addEventListener('dragstart', (e) => this.handleDragStart(e, handleId));
+   }
+   handleDragStart(e, handleId) {
+       this.dragStartSourceId = handleId;
+   }
}
export default HTML5BackendImpl;

12.收集属性 #

12.1 useDrag\index.js #

src\react-dnd\hooks\useDrag\index.js

import useDragSourceMonitor from './useDragSourceMonitor';
import useDragSourceConnector from './useDragSourceConnector';
import useRegisteredDragSource from './useRegisteredDragSource';
import useConnectDragSource from './useConnectDragSource';
+import { useCollectedProps } from '../useCollectedProps';
function useDrag(spec) {
    const monitor = useDragSourceMonitor();
    const connector = useDragSourceConnector();
    useRegisteredDragSource(spec, monitor, connector);
    return [
+       useCollectedProps(monitor, spec.collect),
        useConnectDragSource(connector)
    ];
}
export default useDrag;

12.2 useCollectedProps.js #

src\react-dnd\hooks\useCollectedProps.js

import { useLayoutEffect, useState, useCallback } from 'react';
import equal from 'fast-deep-equal';
export function useCollectedProps(monitor, collect) {
    const [collected, setCollected] = useState(() => collect(monitor));
    const updateCollected = useCallback(() => {
        const nextValue = collect(monitor);
        if (!equal(collected, nextValue)) {
            setCollected(nextValue);
        }
    }, [collect, collected, monitor]);
    useLayoutEffect(function () {
        return monitor.subscribeToStateChange(updateCollected);
    }, [monitor, updateCollected]);
    return collected;
}

12.3 DragSourceMonitorImpl.js #

src\react-dnd\internals\DragSourceMonitorImpl.js

class DragSourceMonitorImpl {
    internalMonitor
    handlerId
    constructor(manager) {
        this.internalMonitor = manager.getGlobalMonitor();
    }
    receiveHandlerId(handlerId) {
        this.handlerId = handlerId;
    }
+   subscribeToStateChange(listener) {
+       return this.internalMonitor.subscribeToStateChange(listener);
+   }
+   isDragging() {
+       return this.internalMonitor.isDraggingSource(this.handlerId);
+   }
}
export default DragSourceMonitorImpl;

12.4 DragDropMonitorImpl.js #

src\dnd-core\classes\DragDropMonitorImpl.js

class DragDropMonitorImpl {
    store
    registry
    constructor(store, registry) {
        this.store = store
        this.registry = registry;
    }
+   subscribeToStateChange(listener) {
+       return this.store.subscribe(listener);
+   }
+   getSourceId() {
+       return 'S0';
+   }
+   isDraggingSource(handlerId) {
+       return handlerId === this.getSourceId();
+   }
}
export default DragDropMonitorImpl;

13.绑定拖动事件 #

13.1 dragOperation.js #

src\dnd-core\reducers\dragOperation.js

import { BEGIN_DRAG, END_DRAG } from '../actions/dragDrop';
const initialState = {
    itemType: null, //拖动元素的类型
    item: null, //拖动的元素
    sourceId: null //来源的ID
};
export function reduce(state = initialState, action) {
    const { payload } = action;
    switch (action.type) {
        case BEGIN_DRAG:
            return {
                ...state,
                itemType: payload.itemType,
                item: payload.item,
                sourceId: payload.sourceId
            };
        case END_DRAG:
            return {
                ...state,
                itemType: null,
                item: null,
                sourceId: null
            };
        default:
            return state;
    }
}

13.2 reducers\index.js #

src\dnd-core\reducers\index.js

+import { reduce as dragOperation, } from './dragOperation';
+import { combineReducers } from 'redux'
+const reducers = {
+    dragOperation
+};
+export default combineReducers(reducers);

13.3 dragDrop\index.js #

src\dnd-core\actions\dragDrop\index.js

import { createBeginDrag } from './beginDrag';
import { createEndDrag } from './endDrag';
export * from './types';
export function createDragDropActions(manager) {
    return {
        beginDrag: createBeginDrag(manager),
        endDrag: createEndDrag(manager),
    };
}

13.4 types.js #

src\dnd-core\actions\dragDrop\types.js

export const BEGIN_DRAG = 'dnd-core/BEGIN_DRAG';
export const END_DRAG = 'dnd-core/END_DRAG';

13.5 beginDrag.js #

src\dnd-core\actions\dragDrop\beginDrag.js

import { BEGIN_DRAG } from './types';
export function createBeginDrag(manager) {
    return function beginDrag(sourceId) {
        const monitor = manager.getGlobalMonitor();
        const registry = manager.getRegistry();
        const source = registry.getSource(sourceId);
        const item = source.beginDrag(monitor, sourceId);
        const itemType = registry.getSourceType(sourceId);
        return {
            type: BEGIN_DRAG,
            payload: {
                itemType,
                item,
                sourceId
            },
        };
    }
}

13.6 DragSourceImpl.js #

src\react-dnd\hooks\useDrag\DragSourceImpl.js

export class DragSourceImpl {
    spec;
    monitor;
    connector;
    constructor(spec, monitor, connector) {
        this.spec = spec;
        this.monitor = monitor;
        this.connector = connector;
    }
+   beginDrag() {
+       const spec = this.spec;
+       const monitor = this.monitor;
+       return spec.item(monitor);
+   }
}

13.7 endDrag.js #

src\dnd-core\actions\dragDrop\endDrag.js

import { END_DRAG } from './types';
export function createEndDrag(manager) {
    return function endDrag() {
        return { type: END_DRAG };
    };
}

13.8 DragDropMonitorImpl.js #

src\dnd-core\classes\DragDropMonitorImpl.js

class DragDropMonitorImpl {
    store
    registry
    constructor(store, registry) {
        this.store = store
        this.registry = registry;
    }
    subscribeToStateChange(listener) {
        return this.store.subscribe(listener);
    }
+   getSourceId() {
+       return this.store.getState().dragOperation.sourceId;
+   }
    isDraggingSource(sourceId) {
        return sourceId === this.getSourceId();
    }
}
export default DragDropMonitorImpl;

13.9 DragDropManagerImpl.js #

src\dnd-core\classes\DragDropManagerImpl.js

+import { createDragDropActions } from '../actions/dragDrop';
+import { bindActionCreators } from 'redux';
class DragDropManagerImpl {
    store;
    backend;
    globalMonitor;
    constructor(store, globalMonitor) {
        this.store = store;
        this.globalMonitor = globalMonitor;
    }
    receiveBackend(backend) {
        this.backend = backend;
+       this.backend.setup();
    }
    getBackend() {
        return this.backend;
    }
    getGlobalMonitor() {
        return this.globalMonitor;
    }
    getRegistry() {
        return this.globalMonitor.registry;
    }
+   getActions() {
+       const actions = createDragDropActions(this);
+       return bindActionCreators(actions, this.store.dispatch);
+   }
}
export default DragDropManagerImpl;

13.10 HTML5BackendImpl.js #

src\react-dnd-html5-backend\HTML5BackendImpl.js

class HTML5BackendImpl {
    dragStartSourceId
+   actions
+   constructor(manager) {
+       this.actions = manager.getActions();
+   }
+   setup() {
+       this.addEventListeners(window);
+   }
+   addEventListeners(target) {
+       target.addEventListener('dragstart', this.handleTopDragStart);
+       target.addEventListener('dragend', this.handleTopDragEndCapture, true);
+   }
+   handleTopDragStart = (e) => {
+       this.actions.beginDrag(this.dragStartSourceId);
+   }
+   handleTopDragEndCapture = () => {
+       this.actions.endDrag();
+   };
    connectDragSource(handleId, node) {
        node.setAttribute('draggable', 'true');
        node.addEventListener('dragstart', (e) => this.handleDragStart(e, handleId));
    }
    handleDragStart(e, handleId) {
        this.dragStartSourceId = handleId;
    }
}
export default HTML5BackendImpl;

14.复制useDrop #

14.1 useDrop\index.js #

src\react-dnd\hooks\useDrop\index.js

import useDropTargetMonitor from './useDropTargetMonitor';
import useDropTargetConnector from './useDropTargetConnector';
import useRegisteredDropTarget from './useRegisteredDropTarget';
import useConnectDropTarget from './useConnectDropTarget';
import { useCollectedProps } from '../useCollectedProps';
function useDrag(spec) {
    const monitor = useDropTargetMonitor();
    const connector = useDropTargetConnector();
    useRegisteredDropTarget(spec, monitor, connector);
    return [
        useCollectedProps(monitor, spec.collect),
        useConnectDropTarget(connector)
    ];
}
export default useDrag;

14.2 useDropTargetMonitor.js #

src\react-dnd\hooks\useDrop\useDropTargetMonitor.js

import { useMemo } from 'react';
import useDragDropManager from '../useDragDropManager';
import { DropTargetMonitorImpl } from '../../internals'
function useDropTargetMonitor() {
    const manager = useDragDropManager();
    return useMemo(() => new DropTargetMonitorImpl(manager), [manager]);
}
export default useDropTargetMonitor;

14.3 DropTargetMonitorImpl.js #

src\react-dnd\internals\DropTargetMonitorImpl.js

class DragSourceMonitorImpl {
    internalMonitor
    handlerId
    constructor(manager) {
        this.internalMonitor = manager.getGlobalMonitor();
    }
    receiveHandlerId(handlerId) {
        this.handlerId = handlerId;
    }
    subscribeToStateChange(listener) {
        return this.internalMonitor.subscribeToStateChange(listener);
    }
    isDragging() {
        return this.internalMonitor.isDraggingSource(this.handlerId);
    }
    getItemType() {
        return this.internalMonitor.getItemType();
    }
    getItem() {
        return this.internalMonitor.getItem();
    }
    getClientOffset() {
        return this.internalMonitor.getClientOffset();
    }
}
export default DragSourceMonitorImpl;

14.4 useDropTargetConnector.js #

src\react-dnd\hooks\useDrop\useDropTargetConnector.js

import { useMemo } from 'react';
import useDragDropManager from '../useDragDropManager';
import { TargetConnector } from '../../internals';
function useDropTargetConnector() {
    const manager = useDragDropManager();
    const connector = useMemo(() => new TargetConnector(manager.getBackend()), [manager]);
    return connector;
}
export default useDropTargetConnector;

14.5 TargetConnector.js #

src\react-dnd\internals\TargetConnector.js

class TargetConnector {
    backend
    handlerId  //拖动源唯一标识
    dropTargetRef //DOM节点
    constructor(backend) {
        this.backend = backend;
    }
    get dropTarget() {
        return this.dropTargetRef && this.dropTargetRef.current;
    }
    connect() {
        if (!this.handlerId || !this.dropTarget) {
            return;
        }
        this.backend.connectDropTarget(this.handlerId, this.dropTarget);
    }

    //useRegisteredDragSource中useLayoutEffect赋值
    receiveHandlerId(handlerId) {
        this.handlerId = handlerId;
        this.connect();
    }
    receiveDropTarget = (dropTargetRef) => {
        this.dropTargetRef = dropTargetRef;
    }
}

export default TargetConnector;

14.6 useRegisteredDropTarget.js #

src\react-dnd\hooks\useDrop\useRegisteredDropTarget.js

import { useLayoutEffect } from 'react';
import useDragDropManager from '../useDragDropManager';
import useDropTarget from './useDropTarget';
import { registerTarget } from '../../internals';
function useRegisteredDropTarget(spec, monitor, connector) {
    const manager = useDragDropManager();
    const handler = useDropTarget(spec, monitor, connector);
    const type = spec.type;
    useLayoutEffect(function () {
        const handlerId = registerTarget(type, handler, manager);
        monitor.receiveHandlerId(handlerId);
        connector.receiveHandlerId(handlerId);
    }, [connector, handler, type, manager, monitor]);

}
export default useRegisteredDropTarget;

14.7 useDropTarget.js #

src\react-dnd\hooks\useDrop\useDropTarget.js

/* eslint-disable react-hooks/exhaustive-deps */
import { useMemo, useEffect } from 'react';
import { DropTargetImpl } from './DropTargetImpl';
function useDropTarget(spec, monitor, connector) {
    const handler = useMemo(() => {
        return new DropTargetImpl(spec, monitor, connector);
    }, [monitor, connector]);
    useEffect(() => {
        handler.spec = spec;
    }, [handler, spec]);
    return handler;
}
export default useDropTarget;

14.8 DropTargetImpl.js #

src\react-dnd\hooks\useDrop\DropTargetImpl.js

export class DropTargetImpl {
    spec;
    monitor;
    connector;
    constructor(spec, monitor, connector) {
        this.spec = spec;
        this.monitor = monitor;
        this.connector = connector;
    }
    hover() {
        const spec = this.spec;
        const monitor = this.monitor;
        if (spec.hover) {
            spec.hover(monitor.getItem(), monitor);
        }
    }
}

14.9 registerTarget.js #

src\react-dnd\internals\registerTarget.js

function registerTarget(type, target, manager) {
    const registry = manager.getRegistry();
    const targetId = registry.addTarget(type, target);
    return targetId;
}
export default registerTarget;

14.10 useConnectDropTarget.js #

src\react-dnd\hooks\useDrop\useConnectDropTarget.js

import { useMemo } from 'react';
function useConnectDropTarget(connector) {
    return useMemo(() => connector.receiveDropTarget, [connector]);
}
export default useConnectDropTarget

14.11 hooks\index.js #

src\react-dnd\hooks\index.js

export { default as useDrag } from './useDrag';
+export { default as useDrop } from './useDrop';

14.12 internals\index.js #

src\react-dnd\internals\index.js

export { default as DragSourceMonitorImpl } from './DragSourceMonitorImpl';
+export { default as DropTargetMonitorImpl } from './DropTargetMonitorImpl';
export { default as SourceConnector } from './SourceConnector';
+export { default as TargetConnector } from './TargetConnector';
export { default as registerSource } from './registerSource';
+export { default as registerTarget } from './registerTarget';

15.增加监听 #

15.1 src\react-dnd-html5-backend\HTML5BackendImpl.js #

src\react-dnd-html5-backend\HTML5BackendImpl.js

+import { getEventClientOffset } from './OffsetUtils';
class HTML5BackendImpl {
    dragStartSourceId
    actions
+   dragOverTargetId
    constructor(manager) {
        this.actions = manager.getActions();
    }
    setup() {
        this.addEventListeners(window);
    }
    addEventListeners(target) {
        target.addEventListener('dragstart', this.handleTopDragStart);
+       target.addEventListener('dragover', this.handleTopDragOver);
        target.addEventListener('dragend', this.handleTopDragEndCapture, true);
    }
    handleTopDragStart = (e) => {
        this.actions.beginDrag(this.dragStartSourceId);
    }
    handleTopDragEndCapture = () => {
        this.actions.endDrag();
    };
    connectDragSource(handleId, node) {
        node.setAttribute('draggable', 'true');
        node.addEventListener('dragstart', (e) => this.handleDragStart(e, handleId));
    }
    handleDragStart(e, handleId) {
        this.dragStartSourceId = handleId;
    }
+   handleTopDragOver = (e) => {
+       this.actions.hover(this.dragOverTargetId, {
+           clientOffset: getEventClientOffset(e)
+       });
+   };
+   handleDragOver(e, targetId) {
+       this.dragOverTargetId = targetId;
+   }
+   connectDropTarget(targetId, node) {
+       console.log('connectDropTarget', targetId);
+       node.addEventListener('dragover', (e) => this.handleDragOver(e, targetId));
+   }
}
export default HTML5BackendImpl;

15.2 OffsetUtils.js #

src\react-dnd-html5-backend\OffsetUtils.js

/**
 * 获取事件在浏览器可视区域的坐标
 * @param {*} e 
 * @returns 
 */
export function getEventClientOffset(e) {
    return {
        x: e.clientX,//鼠标相对于浏览器窗口可视区域的X
        y: e.clientY//鼠标相对于浏览器窗口可视区域的Y坐标
    };
}

16.注册和监听 #

16.1 src\dnd-core\classes\DragDropMonitorImpl.js #

src\dnd-core\classes\DragDropMonitorImpl.js

class DragDropMonitorImpl {
    store
    registry
    constructor(store, registry) {
        this.store = store
        this.registry = registry;
    }
    subscribeToStateChange(listener) {
        return this.store.subscribe(listener);
    }
    getSourceId() {
        return this.store.getState().dragOperation.sourceId;
    }
    isDraggingSource(sourceId) {
        return sourceId === this.getSourceId();
    }
+   getItem() {
+       return this.store.getState().dragOperation.item;
+   }
+   getItemType() {
+       return this.store.getState().dragOperation.itemType;
+   }
+   getClientOffset() {
+       return this.store.getState().dragOffset.clientOffset;
+   }
}
export default DragDropMonitorImpl;

16.2 src\dnd-core\classes\HandlerRegistryImpl.js #

src\dnd-core\classes\HandlerRegistryImpl.js

import { HandlerRole } from '../interfaces';
import { getNextUniqueId } from '../utils';
+import { addSource, addTarget } from '../actions/registry';
class HandlerRegistryImpl {
    store;//redux仓库
    types = new Map(); //不同handlerId的类型
    dragSources = new Map();//拖放来源
+   dropTargets = new Map();//放置目标
    constructor(store) {
        this.store = store;
    }
    addSource(type, source) {
        const handlerId = this.addHandler(HandlerRole.SOURCE, type, source);
        this.store.dispatch(addSource(handlerId));
        return handlerId;
    }
    addHandler(role, type, handler) {
        const handlerId = getNextHandlerId(role);//获取下一个handleId
        this.types.set(handlerId, type);
        if (role === HandlerRole.SOURCE) {
            this.dragSources.set(handlerId, handler);
+       } else if (role === HandlerRole.TARGET) {
+           this.dropTargets.set(handlerId, handler);
+       }
        return handlerId;
    }
    getSource(handlerId) {
        return this.dragSources.get(handlerId);
    }
    getSourceType(handlerId) {
        return this.types.get(handlerId);
    }
+   addTarget(type, target) {
+       const targetId = this.addHandler(HandlerRole.TARGET, type, target);
+       this.store.dispatch(addTarget(targetId));
+       return targetId;
+   }
+   getTarget(targetId) {
+       return this.dropTargets.get(targetId);
+   }
+   getTargetType(targetId) {
+       return this.types.get(targetId);
+   }
}
function getNextHandlerId(role) {
    const id = getNextUniqueId().toString();
    switch (role) {
        case HandlerRole.SOURCE:
            return `S${id}`;
        case HandlerRole.TARGET:
            return `T${id}`;
        default:
            throw new Error(`Unknown Handler Role: ${role}`);
    }
}
export default HandlerRegistryImpl;

16.3 src\dnd-core\actions\registry.js #

src\dnd-core\actions\registry.js

export const ADD_SOURCE = 'dnd-core/ADD_SOURCE';
+export const ADD_TARGET = 'dnd-core/ADD_TARGET';
export function addSource(handlerId) {
    return {
        type: ADD_SOURCE,
        payload: { handlerId }
    };
}
+export function addTarget(targetId) {
+    return {
+        type: ADD_TARGET,
+        payload: {
+            targetId
+        },
+    };
+}

17.store #

17.1 reducers\index.js #

src\dnd-core\reducers\index.js

import { reduce as dragOperation, } from './dragOperation';
+import { reduce as dragOffset } from './dragOffset';
import { combineReducers } from 'redux'
const reducers = {
    dragOperation,
+   dragOffset
};
export default combineReducers(reducers);

17.2 dragOffset.js #

src\dnd-core\reducers\dragOffset.js

import { BEGIN_DRAG, HOVER, END_DRAG } from '../actions/dragDrop';
const initialState = {
    clientOffset: {},
};
export function reduce(state = initialState, action) {
    const { payload } = action;
    switch (action.type) {
        case BEGIN_DRAG:
            return {
                clientOffset: payload.clientOffset,
            };
        case HOVER:
            return {
                clientOffset: payload.clientOffset
            };
        case END_DRAG:
            return initialState;
        default:
            return state;
    }
}

17.3 dragDrop\index.js #

src\dnd-core\actions\dragDrop\index.js

import { createBeginDrag } from './beginDrag';
import { createEndDrag } from './endDrag';
+import { createHover } from './hover';
export * from './types';
export function createDragDropActions(manager) {
    return {
        beginDrag: createBeginDrag(manager),
        endDrag: createEndDrag(manager),
+       hover: createHover(manager)
    };
}

17.4 hover.js #

src\dnd-core\actions\dragDrop\hover.js

import { HOVER } from './types';
export function createHover(manager) {
    return function hover(targetId, { clientOffset } = {}) {
        const registry = manager.getRegistry();//HandlerRegistryImpl
        const target = registry.getTarget(targetId);//DropTargetImpl
        queueMicrotask(target.hover.bind(target));
        return {
            type: HOVER,
            payload: {
                targetId,
                clientOffset
            },
        };
    };
}

17.5 src\dnd-core\actions\dragDrop\types.js #

src\dnd-core\actions\dragDrop\types.js

export const BEGIN_DRAG = 'dnd-core/BEGIN_DRAG';
export const END_DRAG = 'dnd-core/END_DRAG';
+export const HOVER = 'dnd-core/HOVER';