一百多行代码实现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 相关文章推荐
学习YUI.Ext 第二天
Mar 10 Javascript
浅说js变量
May 25 Javascript
IE下使用cloneNode注意事项分享
Nov 22 Javascript
浏览器兼容console对象的简要解决方案分享
Oct 24 Javascript
Jquery的基本对象转换和文档加载用法实例
Feb 25 Javascript
jquery对象和DOM对象的相互转换详解
Oct 18 Javascript
BootStrap整体框架之基础布局组件
Dec 15 Javascript
jQuery插件HighCharts实现的2D堆条状图效果示例【附demo源码下载】
Mar 14 Javascript
jquery dataTable 后台加载数据并分页实例代码
Jun 07 jQuery
微信小程序-getUserInfo回调的实例详解
Oct 27 Javascript
详解vue-router 命名路由和命名视图
Jun 01 Javascript
使用vue-element-admin框架从后端动态获取菜单功能的实现
Apr 29 Vue.js
node中使用shell脚本的方法步骤
详解如何解决使用JSON.stringify时遇到的循环引用问题
vue 中 get / delete 传递数组参数方法
Mar 23 #Vue.js
JavaScript实现页面动态验证码的实现示例
使用Vue.js和MJML创建响应式电子邮件
JS原生实现轮播图的几种方法
几款主流好用的富文本编辑器(所见即所得常用编辑器)介绍
You might like
PHP使用者状态管理功能的应用
2006/10/09 PHP
PHP程序员编程注意事项
2008/04/10 PHP
php获取当月最后一天函数分享
2015/02/02 PHP
php实用代码片段整理
2016/11/12 PHP
Javascript技巧之不要用for in语句对数组进行遍历
2010/10/20 Javascript
JS和函数式语言的三特性
2014/03/05 Javascript
jQuery中ajax的load()与post()方法实例详解
2016/01/05 Javascript
javascript HTML5 canvas实现打砖块游戏
2020/06/18 Javascript
关于 jQuery Easyui异步加载tree的问题解析
2016/12/06 Javascript
jquery平滑滚动到顶部插件使用详解
2017/05/08 jQuery
JavaScript获取用户所在城市及地理位置
2018/04/21 Javascript
jquery简单实现纵向的无缝滚动代码实例
2019/04/01 jQuery
微信小程序移动拖拽视图-movable-view实例详解
2019/08/17 Javascript
JS中FormData类实现文件上传
2020/03/27 Javascript
python Django连接MySQL数据库做增删改查
2013/11/07 Python
Python在图片中插入大量文字并且自动换行
2019/01/02 Python
linux安装python修改默认python版本方法
2019/03/31 Python
python利用selenium进行浏览器爬虫
2019/04/25 Python
对pyqt5多线程正确的开启姿势详解
2019/06/14 Python
python绘制双Y轴折线图以及单Y轴双变量柱状图的实例
2019/07/08 Python
Python如何实现强制数据类型转换
2019/11/22 Python
在pytorch中对非叶节点的变量计算梯度实例
2020/01/10 Python
Python实现加密接口测试方法步骤详解
2020/06/05 Python
python如何实时获取tcpdump输出
2020/09/16 Python
Pytest测试框架基本使用方法详解
2020/11/25 Python
Charlotte Tilbury美国官网:英国美妆品牌
2017/10/13 全球购物
英国领先的亚洲旅游专家:Wendy Wu Tours
2018/01/21 全球购物
2014年教师培训的自我评价
2014/01/03 职场文书
《卖木雕的少年》教学反思
2014/04/11 职场文书
企业安全生产责任书
2014/04/14 职场文书
普通党员群众路线教育实践活动心得体会
2014/11/04 职场文书
2015年感恩节演讲稿(优选篇)
2015/03/20 职场文书
会议室管理制度范本
2015/08/06 职场文书
安全生产标语口号
2015/12/26 职场文书
golang为什么要统一错误处理
2022/04/03 Golang
使用Python通过企业微信应用给企业成员发消息
2022/04/18 Python