一百多行代碼實(shí)現(xiàn)react拖拽hooks
前言
源碼總共也就一百多行,看完這個(gè)大致可以理解一些成熟的react拖拽庫(kù)的實(shí)現(xiàn)思路,比如react-dnd,然后你上手這些庫(kù)的時(shí)候就非常快了。
使用hooks實(shí)現(xiàn)的大致效果動(dòng)圖如下:

我們的目標(biāo)是實(shí)現(xiàn)一個(gè)useDrag和useDrop的hooks,類似以下用法就可以輕松讓元素可以拖拽,并且在拖拽的各個(gè)生命周期,如下,可以自定義傳遞消息(順便介紹幾個(gè)拖拽會(huì)觸發(fā)的事件)。
- dragstart:用戶開始拖拉時(shí),在被拖拉的節(jié)點(diǎn)上觸發(fā),該事件的target屬性是被拖拉的節(jié)點(diǎn)。
- dragenter:拖拉進(jìn)入當(dāng)前節(jié)點(diǎn)時(shí),在當(dāng)前節(jié)點(diǎn)上觸發(fā)一次,該事件的target屬性是當(dāng)前節(jié)點(diǎn)。通常應(yīng)該在這個(gè)事件的監(jiān)聽函數(shù)中,指定是否允許在當(dāng)前節(jié)點(diǎn)放下(drop)拖拉的數(shù)據(jù)。如果當(dāng)前節(jié)點(diǎn)沒有該事件的監(jiān)聽函數(shù),或者監(jiān)聽函數(shù)不執(zhí)行任何操作,就意味著不允許在當(dāng)前節(jié)點(diǎn)放下數(shù)據(jù)。在視覺上顯示拖拉進(jìn)入當(dāng)前節(jié)點(diǎn),也是在這個(gè)事件的監(jiān)聽函數(shù)中設(shè)置。
- dragover:拖拉到當(dāng)前節(jié)點(diǎn)上方時(shí),在當(dāng)前節(jié)點(diǎn)上持續(xù)觸發(fā)(相隔幾百毫秒),該事件的target屬性是當(dāng)前節(jié)點(diǎn)。該事件與dragenter事件的區(qū)別是,dragenter事件在進(jìn)入該節(jié)點(diǎn)時(shí)觸發(fā),然后只要沒有離開這個(gè)節(jié)點(diǎn),dragover事件會(huì)持續(xù)觸發(fā)。
- dragleave:拖拉操作離開當(dāng)前節(jié)點(diǎn)范圍時(shí),在當(dāng)前節(jié)點(diǎn)上觸發(fā),該事件的target屬性是當(dāng)前節(jié)點(diǎn)。如果要在視覺上顯示拖拉離開操作當(dāng)前節(jié)點(diǎn),就在這個(gè)事件的監(jiān)聽函數(shù)中設(shè)置。
使用方法 + 源碼講解
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的組件傳遞消息,比如當(dāng)前拖拽的元素是那個(gè)dom,或者你想要其他信息都可以往里面加,我們看看它的實(shí)現(xiàn)。
const DragAndDropContext = React.createContext({ DragAndDropManager: {} });
const DragAndDrop = ({ children }) => (
<DragAndDropContext.Provider value={{ DragAndDropManager: new DragAndDropManager() }}>
{children}
</DragAndDropContext.Provider>
)
可以看到傳遞消息是用react的Context的api去實(shí)現(xiàn)的,重點(diǎn)就是這個(gè)DragAndDropManager,我們看下實(shí)現(xiàn)
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的作用是用來(lái)記錄當(dāng)前drag的元素是哪個(gè),useDrag里面會(huì)用到,我們?cè)诳磚seDrag的hooks實(shí)現(xiàn)的時(shí)候就會(huì)明白只要調(diào)用setActive方法把drag的dom元素傳進(jìn)去,是不是就知道當(dāng)前拖拽的元素是哪個(gè)了呢。
除此之外,我還增加了訂閱事件的api,subscribe,目前我并沒有使用它,本次示例里你可以忽略這部分,知道可以添加訂閱事件就行。
接著我們看看,useDrag的使用,DragElement的實(shí)現(xiàn)如下:
function DragElement() {
const input = useRef(null)
const hanleDrag = useDrag({
ref: input,
collection: {}, // 這里可以填寫任意你想傳遞給drop元素的消息,后面會(huì)通過(guò)參數(shù)的形式傳遞給drop元素
})
return (
<div ref={input}>
<h1 role="button" onClick={hanleDrag}>
drag元素
</h1>
</div>
)
}
我們就來(lái)看下useDrag的實(shí)現(xiàn),非常簡(jiǎn)單
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做的事情非常簡(jiǎn)單,
- 首先通過(guò)useContext,來(lái)把獲取最外層store的數(shù)據(jù),也就是上面代碼的DragAndDropManager
- 在useEffect里面,如果外界傳入了ref,就將這個(gè)dom元素的屬性draggable設(shè)為true,也就是可拖拽狀態(tài)
- 然后給這個(gè)元素綁定dragstart事件,注意了,銷毀組件的時(shí)候我們要移除事件,以防內(nèi)存泄漏
- handleDragStart事件首先把外界傳的props.collection更新到我們的外界倉(cāng)庫(kù)里,這樣每一個(gè)要drag,也就是拖拽的元素都可以將我們useDrag中傳是入的useDrag({collection: {}})信息,通過(guò)DragAndDropManager.setActive(props.collection)的方式,傳入到外界的store
- 接著我們dataTransder屬性上做一些事,目的是設(shè)置元素的拖拽屬性為move,并且為了兼容firefox做了處理。
- 最后每當(dāng)出發(fā)drag事件的時(shí)候,外界傳入的onDragStart事件也會(huì)觸發(fā),并且我們將store里的數(shù)據(jù)傳入進(jìn)去
其中,useDrop的使用,DropElement的實(shí)現(xiàn)如下:
function DropElement(props: any): any {
const input = useRef(null)
useDrop({
ref: input,
// e代表dragOver事件發(fā)生時(shí),正在被over的元素的event對(duì)象
// collection是store存儲(chǔ)的數(shù)據(jù)
// showAfter是表示,是否鼠標(biāo)拖拽元素時(shí),鼠標(biāo)經(jīng)過(guò)drop元素的上方(上方就是上半邊,下方就是下半邊)
onDragOver: (e, collection, showAfter) => {
// 如果經(jīng)過(guò)上半邊,drop元素的上邊框就是紅色
if (!showAfter) {
input.current.style = "border-bottom: none;border-top: 1px solid red"
} else {
// 如果經(jīng)過(guò)下半邊,drop元素的上邊框就是紅色
input.current.style = "border-top: none;border-bottom: 1px solid red"
}
},
// 如果在drop元素上放開鼠標(biāo),則樣式清空
onDrop: () => {
input.current.style = ""
},
// 如果在離開drop元素,則樣式清空
onDragLeave: () => {
input.current.style = ""
},
})
return (
<div>
<h1 ref={input}>drop元素</h1>
</div>
)
}
最后,我們來(lái)看看useDrop的實(shí)現(xiàn)
export default function useDrop(props) {
// 獲取最外層store里的數(shù)據(jù)
const { DragAndDropManager } = useContext(DragAndDropContext)
const handleDragOver = (e) => {
// e就是拖拽的event對(duì)象
e.preventDefault()
// getBoundingClientRect的圖請(qǐng)看下面
const overElementHeight = e.currentTarget.getBoundingClientRect().height / 2
const overElementTopOffset = e.currentTarget.getBoundingClientRect().top
// clientY就是鼠標(biāo)到瀏覽器頁(yè)面可視區(qū)域的最頂端的距離
const mousePositionY = e.clientY
// mousePositionY - overElementTopOffset就是鼠標(biāo)在元素內(nèi)部到元素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)
}
}
// 注冊(cè)事件,注意銷毀組件時(shí)要注銷事件,避免內(nèi)存泄露
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:元素左邊到視窗左邊的距離;

到此這篇關(guān)于一百多行代碼實(shí)現(xiàn)react拖拽hooks的文章就介紹到這了,更多相關(guān)react拖拽hooks內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
解決React報(bào)錯(cuò)`value` prop on `input` should&
這篇文章主要為大家介紹了React報(bào)錯(cuò)`value` prop on `input` should not be null解決方法詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-12-12
詳解使用webpack+electron+reactJs開發(fā)windows桌面應(yīng)用
這篇文章主要介紹了詳解使用webpack+electron+reactJs開發(fā)windows桌面應(yīng)用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2019-02-02
React項(xiàng)目中應(yīng)用TypeScript的實(shí)現(xiàn)
TypeScript通常都會(huì)依賴于框架,例如和vue、react 這些框架結(jié)合,本文就主要介紹了React項(xiàng)目中應(yīng)用TypeScript的實(shí)現(xiàn),分享給大家,具體如下:2021-09-09
關(guān)于React中setState同步或異步問題的理解
相信很多小伙伴們都一直在疑惑,setState 到底是同步還是異步。本文就詳細(xì)的介紹一下React中setState同步或異步問題,感興趣的可以了解一下2021-11-11
React實(shí)現(xiàn)頁(yè)面狀態(tài)緩存(keep-alive)的示例代碼
因?yàn)?react、vue都是單頁(yè)面應(yīng)用,路由跳轉(zhuǎn)時(shí),就會(huì)銷毀上一個(gè)頁(yè)面的組件,但是有些項(xiàng)目不想被銷毀,想保存狀態(tài),本文給大家介紹了React實(shí)現(xiàn)頁(yè)面狀態(tài)緩存(keep-alive)的代碼示例,需要的朋友可以參考下2024-01-01
React踩坑之a(chǎn)ntd輸入框rules中的required=true問題
這篇文章主要介紹了React踩坑之a(chǎn)ntd輸入框rules中的required=true問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2023-06-06
使用react-beautiful-dnd實(shí)現(xiàn)列表間拖拽踩坑
相比于react-dnd,react-beautiful-dnd更適用于列表之間拖拽的場(chǎng)景,本文主要介紹了使用react-beautiful-dnd實(shí)現(xiàn)列表間拖拽踩坑,感興趣的可以了解一下2021-05-05

