一百多行代码实现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 相关文章推荐
父窗口获取弹出子窗口文本框的值
Jun 27 Javascript
javascript 树控件 比较好用
Jun 11 Javascript
js中parseFloat(参数1,参数2)定义和用法及注意事项
Jan 27 Javascript
JS动态添加与删除select中的Option对象(示例代码)
Dec 25 Javascript
jQuery+PHP+MySQL二级联动下拉菜单实例讲解
Oct 27 Javascript
jquery制作属于自己的select自定义样式
Nov 23 Javascript
基于JS实现数字+字母+中文的混合排序方法
Jun 06 Javascript
ionic实现滑动的三种方式
Aug 27 Javascript
使用jQuery.Pin垂直滚动时固定导航
May 24 jQuery
玩转Koa之核心原理分析
Dec 29 Javascript
vue动态绘制四分之三圆环图效果
Sep 03 Javascript
vue点击按钮动态创建与删除组件功能
Dec 29 Javascript
node中使用shell脚本的方法步骤
详解如何解决使用JSON.stringify时遇到的循环引用问题
vue 中 get / delete 传递数组参数方法
Mar 23 #Vue.js
JavaScript实现页面动态验证码的实现示例
使用Vue.js和MJML创建响应式电子邮件
JS原生实现轮播图的几种方法
几款主流好用的富文本编辑器(所见即所得常用编辑器)介绍
You might like
用文本文件实现的动态实时发布新闻的程序
2006/10/09 PHP
PHP设置图片文件上传大小的具体实现方法
2013/10/11 PHP
PHP连接SQLServer2005方法及代码
2013/12/26 PHP
php使用date和strtotime函数输出指定日期的方法
2014/11/14 PHP
PHP使用get_headers函数判断远程文件是否存在的方法
2014/11/28 PHP
php单例模式示例分享
2015/02/12 PHP
网页前台通过js非法字符过滤代码(骂人的话等等)
2010/05/26 Javascript
js通过更改按钮的显示样式实现按钮的滑动效果
2014/04/23 Javascript
提升jQuery的性能需要做好七件事
2016/01/11 Javascript
JS 实现Base64编码与解码实例详解
2016/11/07 Javascript
jQuery实现简易的输入框字数计数功能示例
2017/01/16 Javascript
Vue 2.0在IE11中打开项目页面空白的问题解决
2017/07/16 Javascript
从对象列表中获取一个对象的方法,依据关键字和值
2017/09/20 Javascript
Vue.js 2.5新特性介绍(推荐)
2017/10/24 Javascript
Node.js文件编码格式的转换的方法
2018/04/27 Javascript
原生js实现淘宝放大镜效果
2020/10/28 Javascript
vue-cli或vue项目利用HBuilder打包成移动端app操作
2020/07/29 Javascript
JS+CSS实现动态时钟
2021/02/19 Javascript
[02:15]你好,这就是DOTA!
2015/08/05 DOTA
压缩包密码破解示例分享(类似典破解)
2014/01/17 Python
pyqt4教程之实现半透明的天气预报界面示例
2014/03/02 Python
Python中字符串的处理技巧分享
2016/09/17 Python
详解使用 pyenv 管理多个版本 python 环境
2017/10/19 Python
Python3单行定义多个变量或赋值方法
2018/07/12 Python
Python模拟百度自动输入搜索功能的实例
2019/02/14 Python
Python3使用xml.dom.minidom和xml.etree模块儿解析xml文件封装函数的方法
2019/09/23 Python
在PyCharm中实现添加快捷模块
2020/02/12 Python
Python面向对象特殊属性及方法解析
2020/09/16 Python
奥斯汀独木舟和皮划艇:Austin Canoe & Kayak
2018/05/22 全球购物
计算机专业个人简短的自我评价
2013/10/23 职场文书
大学军训感言1500字
2014/03/09 职场文书
质量安全标语
2014/06/07 职场文书
2016年小学端午节活动总结
2016/04/01 职场文书
有关花店创业的计划书模板
2019/08/27 职场文书
关于拾金不昧的感谢信(五篇)
2019/10/18 职场文书
Python实现学生管理系统并生成exe可执行文件详解流程
2022/01/22 Python