一百多行代码实现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 相关文章推荐
Prototype使用指南之ajax
Jan 10 Javascript
js 金额文本框实现代码
Feb 14 Javascript
jquery.pagination.js 无刷新分页实现步骤分享
May 23 Javascript
谈谈JavaScript中的函数与闭包
Apr 14 Javascript
javascript上下方向键控制表格行选中并高亮显示的方法
Feb 13 Javascript
node.js 使用ejs模板引擎时后缀换成.html
Apr 22 Javascript
JavaScript中string对象
Jun 12 Javascript
JavaScript实现同一页面内两个表单互相传值的方法
Aug 12 Javascript
jQuery实现拖动剪裁图片作为头像
Dec 28 Javascript
JS实现中国公民身份证号码有效性验证
Feb 20 Javascript
js判断复选框是否选中的方法示例【基于jQuery】
Oct 10 jQuery
小程序实现多个选项卡切换
Jun 19 Javascript
node中使用shell脚本的方法步骤
详解如何解决使用JSON.stringify时遇到的循环引用问题
vue 中 get / delete 传递数组参数方法
Mar 23 #Vue.js
JavaScript实现页面动态验证码的实现示例
使用Vue.js和MJML创建响应式电子邮件
JS原生实现轮播图的几种方法
几款主流好用的富文本编辑器(所见即所得常用编辑器)介绍
You might like
用PHP为SHOPEX增加日志功能代码
2010/07/02 PHP
php下正则来匹配dede模板标签的代码
2010/08/21 PHP
Linux下CoreSeek及PHP扩展模块的安装
2012/09/23 PHP
PHP读取PDF内容配合Xpdf的使用
2012/11/24 PHP
日常整理PHP中简单的图形处理(经典)
2015/10/26 PHP
一个非常实用的php文件上传类
2017/07/04 PHP
location.href用法总结(最主要的)
2013/12/27 Javascript
javascript实现iframe框架延时加载的方法
2014/10/30 Javascript
JavaScript中Null与Undefined的区别解析
2015/06/30 Javascript
实现JavaScript的组成----BOM和DOM详解
2016/05/18 Javascript
JQuery实现DIV其他动画效果的简单实例
2016/09/18 Javascript
EasyUI 中combotree 默认不能选择父节点的实现方法
2016/11/07 Javascript
Angular 4依赖注入学习教程之InjectToken的使用(八)
2017/06/04 Javascript
关于vue表单提交防双/多击的例子
2019/10/31 Javascript
jQuery三组基本动画与自定义动画操作实例总结
2020/05/09 jQuery
springboot+vue+对接支付宝接口+二维码扫描支付功能(沙箱环境)
2020/10/15 Javascript
python中如何使用正则表达式的非贪婪模式示例
2017/10/09 Python
python抽取指定url页面的title方法
2018/05/11 Python
flask中过滤器的使用详解
2018/08/01 Python
django将网络中的图片,保存成model中的ImageField的实例
2019/08/07 Python
python matplotlib饼状图参数及用法解析
2019/11/04 Python
python保留格式汇总各部门excel内容的实现思路
2020/06/01 Python
利用HTML5中的Canvas绘制一张笑脸的教程
2015/05/07 HTML / CSS
草莓网美国官网:Strawberrynet USA
2016/12/11 全球购物
Overload和Override的区别。Overloaded的方法是否可以改变返回值的类型
2013/10/30 面试题
2013年大学生的自我鉴定
2013/10/24 职场文书
心理学专业毕业生推荐信范文
2013/11/21 职场文书
党的群众路线教育实践活动公开承诺书
2014/03/28 职场文书
校园文明倡议书
2014/05/16 职场文书
单位委托函范文
2015/01/29 职场文书
2015年保险公司内勤工作总结
2015/05/23 职场文书
中学音乐课教学反思
2016/02/18 职场文书
2019年教师入党申请书
2019/06/27 职场文书
写一个Python脚本下载哔哩哔哩舞蹈区的所有视频
2021/05/31 Python
5种 JavaScript 方式实现数组扁平化
2021/10/05 Javascript
用Python生成会跳舞的美女
2022/01/18 Python