一百多行代码实现react拖拽hooks


Posted in Javascript onMarch 23, 2021

前言

源码总共也就一百多行,看完这个大致可以理解一些成熟的react拖拽库的实现思路,比如react-dnd,然后你上手这些库的时候就非常快了。

使用hooks实现的大致效果动图如下:

一百多行代码实现react拖拽hooks

我们的目标是实现一个useDrag和useDrop的hooks,类似以下用法就可以轻松让元素可以拖拽,并且在拖拽的各个生命周期,如下,可以自定义传递消息(顺便介绍几个拖拽会触发的事件)。

  • dragstart:用户开始拖拉时,在被拖拉的节点上触发,该事件的target属性是被拖拉的节点。
  • dragenter:拖拉进入当前节点时,在当前节点上触发一次,该事件的target属性是当前节点。通常应该在这个事件的监听函数中,指定是否允许在当前节点放下(drop)拖拉的数据。如果当前节点没有该事件的监听函数,或者监听函数不执行任何操作,就意味着不允许在当前节点放下数据。在视觉上显示拖拉进入当前节点,也是在这个事件的监听函数中设置。
  • dragover:拖拉到当前节点上方时,在当前节点上持续触发(相隔几百毫秒),该事件的target属性是当前节点。该事件与dragenter事件的区别是,dragenter事件在进入该节点时触发,然后只要没有离开这个节点,dragover事件会持续触发。
  • dragleave:拖拉操作离开当前节点范围时,在当前节点上触发,该事件的target属性是当前节点。如果要在视觉上显示拖拉离开操作当前节点,就在这个事件的监听函数中设置。

使用方法 + 源码讲解

class Hello extends React.Component<any, any> {
 constructor(props: any) {
  super(props)
  this.state = {}
 }
 
 render() {
  return (
   <DragAndDrop>
    <DragElement />
    <DropElement />
   </DragAndDrop>
  )
 }
}
 
ReactDOM.render(<Hello />, window.document.getElementById("root"))

如上,DragAndDrop组件的作用是给所有的使用useDrag和useDrop的组件传递消息,比如当前拖拽的元素是那个dom,或者你想要其他信息都可以往里面加,我们看看它的实现。

const DragAndDropContext = React.createContext({ DragAndDropManager: {} });
const DragAndDrop = ({ children }) => (
 <DragAndDropContext.Provider value={{ DragAndDropManager: new DragAndDropManager() }}>
  {children}
 </DragAndDropContext.Provider>
)

可以看到传递消息是用react的Context的api去实现的,重点就是这个DragAndDropManager,我们看下实现

export default class DragAndDropManager {
 
 constructor() {
  this.active = null
  this.subscriptions = []
  this.id = -1
 }
 
 setActive(activeProps) {
  this.active = activeProps
  this.subscriptions.forEach((subscription) => subscription.callback())
 }
 
 subscribe(callback) {
  this.id += 1
  this.subscriptions.push({
   callback,
   id: this.id,
  })
 
  return this.id
 }
 
 unsubscribe(id) {
  this.subscriptions = this.subscriptions.filter((sub) => sub.id !== id)
 }
}

setActive的作用是用来记录当前drag的元素是哪个,useDrag里面会用到,我们在看useDrag的hooks实现的时候就会明白只要调用setActive方法把drag的dom元素传进去,是不是就知道当前拖拽的元素是哪个了呢。

除此之外,我还增加了订阅事件的api,subscribe,目前我并没有使用它,本次示例里你可以忽略这部分,知道可以添加订阅事件就行。

接着我们看看,useDrag的使用,DragElement的实现如下:

function DragElement() {
 const input = useRef(null)
 const hanleDrag = useDrag({
  ref: input,
  collection: {}, // 这里可以填写任意你想传递给drop元素的消息,后面会通过参数的形式传递给drop元素
 })
 return (
  <div ref={input}>
   <h1 role="button" onClick={hanleDrag}>
    drag元素
   </h1>
  </div>
 )
}

我们就来看下useDrag的实现,非常简单

export default function useDrag(props) {
 
 const { DragAndDropManager } = useContext(DragAndDropContext)
  
 const handleDragStart = (e) => {
  DragAndDropManager.setActive(props.collection)
  if (e.dataTransfer !== undefined) {
   e.dataTransfer.effectAllowed = "move"
   e.dataTransfer.dropEffect = "move"
   e.dataTransfer.setData("text/plain", "drag") // firefox fix
  }
  if (props.onDragStart) {
   props.onDragStart(DragAndDropManager.active)
  }
 }
  
 useEffect(() => {
  if (!props.ref) return () => {}
  const {
   ref: { current },
  } = props
  if (current) {
   current.setAttribute("draggable", true)
   current.addEventListener("dragstart", handleDragStart)
  }
  return () => {
   current.removeEventListener("dragstart", handleDragStart)
  }
 }, [props.ref.current])
 
 return handleDragStart
}

useDrag做的事情非常简单,

  • 首先通过useContext,来把获取最外层store的数据,也就是上面代码的DragAndDropManager
  • 在useEffect里面,如果外界传入了ref,就将这个dom元素的属性draggable设为true,也就是可拖拽状态
  • 然后给这个元素绑定dragstart事件,注意了,销毁组件的时候我们要移除事件,以防内存泄漏
  • handleDragStart事件首先把外界传的props.collection更新到我们的外界仓库里,这样每一个要drag,也就是拖拽的元素都可以将我们useDrag中传是入的useDrag({collection: {}})信息,通过DragAndDropManager.setActive(props.collection)的方式,传入到外界的store
  • 接着我们dataTransder属性上做一些事,目的是设置元素的拖拽属性为move,并且为了兼容firefox做了处理。
  • 最后每当出发drag事件的时候,外界传入的onDragStart事件也会触发,并且我们将store里的数据传入进去

其中,useDrop的使用,DropElement的实现如下:

function DropElement(props: any): any {
 const input = useRef(null)
 useDrop({
  ref: input,
  // e代表dragOver事件发生时,正在被over的元素的event对象
  // collection是store存储的数据
  // showAfter是表示,是否鼠标拖拽元素时,鼠标经过drop元素的上方(上方就是上半边,下方就是下半边)
  onDragOver: (e, collection, showAfter) => {
  // 如果经过上半边,drop元素的上边框就是红色
   if (!showAfter) {
    input.current.style = "border-bottom: none;border-top: 1px solid red"
   } else {
    // 如果经过下半边,drop元素的上边框就是红色
    input.current.style = "border-top: none;border-bottom: 1px solid red"
   }
  },
  // 如果在drop元素上放开鼠标,则样式清空
  onDrop: () => {
   input.current.style = ""
  },
  // 如果在离开drop元素,则样式清空
  onDragLeave: () => {
   input.current.style = ""
  },
 })
 return (
  <div>
   <h1 ref={input}>drop元素</h1>
  </div>
 )
}

最后,我们来看看useDrop的实现

export default function useDrop(props) {
// 获取最外层store里的数据
 const { DragAndDropManager } = useContext(DragAndDropContext)
 const handleDragOver = (e) => {
 // e就是拖拽的event对象
  e.preventDefault()
  // getBoundingClientRect的图请看下面
  const overElementHeight = e.currentTarget.getBoundingClientRect().height / 2
  const overElementTopOffset = e.currentTarget.getBoundingClientRect().top
  // clientY就是鼠标到浏览器页面可视区域的最顶端的距离
  const mousePositionY = e.clientY
  // mousePositionY - overElementTopOffset就是鼠标在元素内部到元素border-top的距离
  const showAfter = mousePositionY - overElementTopOffset > overElementHeight
  if (props.onDragOver) {
   props.onDragOver(e, DragAndDropManager.active, showAfter)
  }
 }
 // drop事件
 const handledDop = (e: React.DragEvent) => {
  e.preventDefault()
 
  if (props.onDrop) {
   props.onDrop(DragAndDropManager.active)
  }
 }
 // dragLeave事件
 const handledragLeave = (e: React.DragEvent) => {
  e.preventDefault()
 
  if (props.onDragLeave) {
   props.onDragLeave(DragAndDropManager.active)
  }
 }
  // 注册事件,注意销毁组件时要注销事件,避免内存泄露
 useEffect(() => {
  if (!props.ref) return () => {}
  const {
   ref: { current },
  } = props
  if (current) {
   current.addEventListener("dragover", handleDragOver)
   current.addEventListener("drop", handledDop)
   current.addEventListener("dragleave", handledragLeave)
  }
  return () => {
   current.removeEventListener("dragover", handleDragOver)
   current.removeEventListener("drop", handledDop)
   current.removeEventListener("dragleave", handledragLeave)
  }
 }, [props.ref.current])
}

getBoundingClientRect的api图解:

rectObject = object.getBoundingClientRect();

rectObject.top:元素上边到视窗上边的距离;

rectObject.right:元素右边到视窗左边的距离;

rectObject.bottom:元素下边到视窗上边的距离;

rectObject.left:元素左边到视窗左边的距离;

一百多行代码实现react拖拽hooks

Javascript 相关文章推荐
javascript 写类方式之十
Jul 05 Javascript
Mootools 1.2教程 函数
Sep 15 Javascript
jQuery调用AJAX时Get和post公用的乱码解决方法实例说明
Jun 04 Javascript
php显示当前文件所在的文件以及文件夹所有文件以树形展开
Dec 13 Javascript
浅析jQuery(function(){})与(function(){})(jQuery)之间的区别
Jan 09 Javascript
一个简单的实现下拉框多选的插件可移植性比较好
May 05 Javascript
jquery实现仿新浪微博评论滚动效果
Aug 06 Javascript
javascript实现滚动效果的数字时钟实例
Jul 21 Javascript
jQuery ajax方法传递中文时出现中文乱码的解决方法
Jul 25 Javascript
移动开发之自适应手机屏幕宽度
Nov 23 Javascript
javascript实现滚动条效果
Mar 24 Javascript
Vue如何跨组件传递Slot的实现
Dec 14 Vue.js
node中使用shell脚本的方法步骤
详解如何解决使用JSON.stringify时遇到的循环引用问题
vue 中 get / delete 传递数组参数方法
Mar 23 #Vue.js
JavaScript实现页面动态验证码的实现示例
使用Vue.js和MJML创建响应式电子邮件
JS原生实现轮播图的几种方法
几款主流好用的富文本编辑器(所见即所得常用编辑器)介绍
You might like
深入解析PHP中的(伪)多线程与多进程
2013/07/01 PHP
PHP中开启gzip压缩的2种方法
2015/01/31 PHP
session 加入redis的实现代码
2016/07/15 PHP
php版微信公众平台开发之验证步骤实例详解
2016/09/23 PHP
jQuery中DOM操作实例分析
2015/01/23 Javascript
简易的投票系统以及js刷票思路和方法
2015/04/07 Javascript
js中this用法实例详解
2015/05/05 Javascript
微信小程序本地缓存数据增删改查实例详解
2017/05/24 Javascript
jQuery选择器之属性过滤选择器详解
2017/09/28 jQuery
用原生 JS 实现 innerHTML 功能实例详解
2019/04/03 Javascript
createObjectURL方法实现本地图片预览
2019/09/30 Javascript
解决Vue-Router升级导致的Uncaught (in promise)问题
2020/08/07 Javascript
解决vue net :ERR_CONNECTION_REFUSED报错问题
2020/08/13 Javascript
vue 路由meta 设置导航隐藏与显示功能的示例代码
2020/09/04 Javascript
基于vue中的scoped坑点解说
2020/09/04 Javascript
python的三目运算符和not in运算符使用示例
2014/03/03 Python
在Windows中设置Python环境变量的实例讲解
2018/04/28 Python
python 定义n个变量方法 (变量声明自动化)
2018/11/10 Python
django celery redis使用具体实践
2019/04/08 Python
Django REST framework内置路由用法
2019/07/26 Python
浅析PyTorch中nn.Module的使用
2019/08/18 Python
Python 写了个新型冠状病毒疫情传播模拟程序
2020/02/14 Python
Django自关联实现多级联动查询实例
2020/05/19 Python
Python+Appium实现自动化清理微信僵尸好友的方法
2021/02/04 Python
使用Python制作一盏 3D 花灯喜迎元宵佳节
2021/02/26 Python
ghd官网:英国ghd直发器品牌
2018/05/04 全球购物
高级护理专业毕业生推荐信
2013/12/25 职场文书
学校节能减排倡议书
2014/05/16 职场文书
2014年车间主任工作总结
2014/12/10 职场文书
2015年公司行政后勤工作总结
2015/05/20 职场文书
新闻通讯稿范文
2015/07/22 职场文书
教师节获奖感言
2015/07/31 职场文书
学习师德师风的心得体会(2篇)
2019/10/08 职场文书
Python实战之疫苗研发情况可视化
2021/05/18 Python
CSS 伪元素::marker详解
2021/06/26 HTML / CSS
vue cli4中mockjs在dev环境和build环境的配置详情
2022/04/06 Vue.js