一百多行代码实现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 相关文章推荐
List the Codec Files on a Computer
Jun 18 Javascript
js 页面执行时间计算代码
Mar 04 Javascript
jquery时间下拉框小例子
Apr 15 Javascript
Javascript小技能总结(推荐)
Jun 02 Javascript
深入浅析JS是按值传递还是按引用传递(推荐)
Sep 18 Javascript
使用grunt合并压缩js和css文件的方法
Mar 02 Javascript
JavaScript实现移动端页面按手机屏幕分辨率自动缩放的最强代码
Aug 18 Javascript
浅谈使用mpvue开发小程序需要注意和了解的知识点
May 23 Javascript
Vue插值、表达式、分隔符、指令知识小结
Oct 12 Javascript
JS实现判断有效的数独算法示例
Feb 25 Javascript
详解React 元素渲染
Jul 07 Javascript
Vue3不支持Filters过滤器的问题
Sep 24 Javascript
node中使用shell脚本的方法步骤
详解如何解决使用JSON.stringify时遇到的循环引用问题
vue 中 get / delete 传递数组参数方法
Mar 23 #Vue.js
JavaScript实现页面动态验证码的实现示例
使用Vue.js和MJML创建响应式电子邮件
JS原生实现轮播图的几种方法
几款主流好用的富文本编辑器(所见即所得常用编辑器)介绍
You might like
PHP提取数据库内容中的图片地址并循环输出
2010/03/21 PHP
php 操作符与控制结构
2012/03/07 PHP
处理(php-cgi.exe - FastCGI 进程超过了配置的请求超时时限)的问题
2013/07/03 PHP
Opcache导致php-fpm崩溃nginx返回502
2015/03/02 PHP
PHP延迟静态绑定使用方法实例解析
2020/09/05 PHP
javascript据option的value值快速设定初始的selected选项
2007/08/13 Javascript
Jquery 弹出层插件实现代码
2009/10/24 Javascript
jquery 常用操作方法
2010/01/28 Javascript
js常用系统函数用法实例分析
2015/01/12 Javascript
jQuery.Callbacks()回调函数队列用法详解
2016/06/14 Javascript
Extjs让combobox写起来简洁又漂亮
2017/01/05 Javascript
微信小程序-拍照或选择图片并上传文件
2017/01/06 Javascript
深究AngularJS如何获取input的焦点(自定义指令)
2017/06/12 Javascript
Three.js利用顶点绘制立方体的方法详解
2017/09/27 Javascript
nodejs连接mysql数据库及基本知识点详解
2018/03/20 NodeJs
详解vue中的computed的this指向问题
2018/12/05 Javascript
详解vue 在移动端体验上的优化解决方案
2019/05/20 Javascript
layui按条件隐藏表格列的实例
2019/09/19 Javascript
Windows上node.js的多版本管理工具用法实例分析
2019/11/06 Javascript
Python 生成 -1~1 之间的随机数矩阵方法
2018/08/04 Python
Django 表单模型选择框如何使用分组
2019/05/16 Python
Python Pandas对缺失值的处理方法
2019/09/27 Python
Java文件与类动手动脑实例详解
2019/11/10 Python
Python实现投影法分割图像示例(一)
2020/01/17 Python
PIL包中Image模块的convert()函数的具体使用
2020/02/26 Python
python手机号前7位归属地爬虫代码实例
2020/03/31 Python
python字典的值可以修改吗
2020/06/29 Python
python+selenium 简易地疫情信息自动打卡签到功能的实现代码
2020/08/22 Python
使用HTML5 Canvas为图片填充颜色和纹理的教程
2016/03/21 HTML / CSS
施华洛世奇波兰官网:SWAROVSKI波兰
2019/06/18 全球购物
会计专业毕业生自我鉴定
2013/10/29 职场文书
毕业生护理专业个人求职信范文
2014/01/04 职场文书
小学岗位竞聘方案
2014/01/22 职场文书
学校党员对照检查材料
2014/08/28 职场文书
感谢师恩主题班会
2015/08/17 职场文书
Mysql binlog日志文件过大的解决
2021/10/05 MySQL