基于Vue3實現(xiàn)高性能拖拽指令
前言
在現(xiàn)代前端開發(fā)中,拖拽功能是增強用戶體驗的重要手段之一。本文將詳細(xì)介紹如何在 Vue 3 中封裝一個拖拽指令(v-draggable),并通過實戰(zhàn)例子演示其實現(xiàn)過程。通過這篇教程,您將不僅掌握基礎(chǔ)的拖拽功能,還能了解如何優(yōu)化指令以提升其性能和靈活性,從而為您的項目增色。
封裝拖拽指令思路
我們將封裝一個簡單的拖拽指令,名為 v-draggable,它允許我們在任何元素上添加拖拽功能。
指令邏輯
1.監(jiān)聽鼠標(biāo)事件:我們需要監(jiān)聽 mousedown、mousemove 和 mouseup 事件。
2.計算拖動位置:根據(jù)鼠標(biāo)移動的距離更新元素的位置。
3.清理事件:在拖動結(jié)束后移除事件監(jiān)聽器。
實現(xiàn)步驟
第一步:創(chuàng)建指令文件
在 src 目錄下創(chuàng)建一個名為 directives 的文件夾,并在其中創(chuàng)建一個 draggable.js 文件:
// src/directives/draggable.js export default { mounted(el) { el.style.position = 'absolute'; let startX, startY, initialMouseX, initialMouseY; const mousemove = (e) => { const dx = e.clientX - initialMouseX; const dy = e.clientY - initialMouseY; el.style.top = `${startY + dy}px`; el.style.left = `${startX + dx}px`; }; const mouseup = () => { document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); }; el.addEventListener('mousedown', (e) => { startX = el.offsetLeft; startY = el.offsetTop; initialMouseX = e.clientX; initialMouseY = e.clientY; document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', mouseup); e.preventDefault(); }); } };
第二步:注冊指令
在 src 目錄下的 main.js 文件中注冊這個指令:
import { createApp } from 'vue'; import App from './App.vue'; import draggable from './directives/draggable'; const app = createApp(App); app.directive('draggable', draggable); app.mount('#app');
第三步:使用指令
現(xiàn)在我們可以在任何組件中使用這個拖拽指令。編輯 src/App.vue 文件:
<template> <div> <h1>Vue 3 拖拽指令示例</h1> <div v-draggable class="draggable-box">拖拽我!</div> </div> </template> <script> export default { name: 'App' }; </script> <style> .draggable-box { width: 150px; height: 150px; background-color: lightblue; text-align: center; line-height: 150px; cursor: move; user-select: none; } </style>
優(yōu)化拖拽指令
當(dāng)前的拖拽指令已經(jīng)可以基本實現(xiàn)拖拽功能了,但還有一些細(xì)節(jié)需要優(yōu)化,例如:
1.限制拖拽范圍
2.支持觸摸設(shè)備
3.添加節(jié)流來優(yōu)化性能
4.提供一些配置選項
限制拖拽范圍
我們可以通過對元素的位置進行限制,來防止其被拖出指定的范圍。這里我們假定限制在父元素內(nèi)進行拖拽。
// src/directives/draggable.js export default { mounted(el) { el.style.position = 'absolute'; let startX, startY, initialMouseX, initialMouseY; const mousemove = (e) => { const dx = e.clientX - initialMouseX; const dy = e.clientY - initialMouseY; let newTop = startY + dy; let newLeft = startX + dx; // 限制拖拽范圍在父元素內(nèi) const parentRect = el.parentElement.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); if (newLeft < 0) { newLeft = 0; } else if (newLeft + elRect.width > parentRect.width) { newLeft = parentRect.width - elRect.width; } if (newTop < 0) { newTop = 0; } else if (newTop + elRect.height > parentRect.height) { newTop = parentRect.height - elRect.height; } el.style.top = `${newTop}px`; el.style.left = `${newLeft}px`; }; const mouseup = () => { document.removeEventListener('mousemove', mousemove); document.removeEventListener('mouseup', mouseup); }; el.addEventListener('mousedown', (e) => { startX = el.offsetLeft; startY = el.offsetTop; initialMouseX = e.clientX; initialMouseY = e.clientY; document.addEventListener('mousemove', mousemove); document.addEventListener('mouseup', mouseup); e.preventDefault(); }); } };
支持觸摸設(shè)備
為了支持觸摸設(shè)備,我們需要添加 touchstart、touchmove 和 touchend 事件監(jiān)聽器。
// src/directives/draggable.js export default { mounted(el) { el.style.position = 'absolute'; let startX, startY, initialMouseX, initialMouseY; const move = (e) => { let clientX, clientY; if (e.touches) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const dx = clientX - initialMouseX; const dy = clientY - initialMouseY; let newTop = startY + dy; let newLeft = startX + dx; const parentRect = el.parentElement.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); if (newLeft < 0) { newLeft = 0; } else if (newLeft + elRect.width > parentRect.width) { newLeft = parentRect.width - elRect.width; } if (newTop < 0) { newTop = 0; } else if (newTop + elRect.height > parentRect.height) { newTop = parentRect.height - elRect.height; } el.style.top = `${newTop}px`; el.style.left = `${newLeft}px`; }; const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); document.removeEventListener('touchmove', move); document.removeEventListener('touchend', up); }; const down = (e) => { startX = el.offsetLeft; startY = el.offsetTop; if (e.touches) { initialMouseX = e.touches[0].clientX; initialMouseY = e.touches[0].clientY; } else { initialMouseX = e.clientX; initialMouseY = e.clientY; } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); document.addEventListener('touchmove', move); document.addEventListener('touchend', up); e.preventDefault(); }; el.addEventListener('mousedown', down); el.addEventListener('touchstart', down); } };
添加節(jié)流優(yōu)化性能
為了防止 mousemove 和 touchmove 事件觸發(fā)得太頻繁,我們可以使用節(jié)流(throttle)技術(shù)來優(yōu)化性能。
// src/directives/draggable.js function throttle(func, limit) { let lastFunc; let lastRan; return function (...args) { const context = this; if (!lastRan) { func.apply(context, args); lastRan = Date.now(); } else { clearTimeout(lastFunc); lastFunc = setTimeout(function () { if (Date.now() - lastRan >= limit) { func.apply(context, args); lastRan = Date.now(); } }, limit - (Date.now() - lastRan)); } }; } export default { mounted(el) { el.style.position = 'absolute'; let startX, startY, initialMouseX, initialMouseY; const move = throttle((e) => { let clientX, clientY; if (e.touches) { clientX = e.touches[0].clientX; clientY = e.touches[0].clientY; } else { clientX = e.clientX; clientY = e.clientY; } const dx = clientX - initialMouseX; const dy = clientY - initialMouseY; let newTop = startY + dy; let newLeft = startX + dx; const parentRect = el.parentElement.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); if (newLeft < 0) { newLeft = 0; } else if (newLeft + elRect.width > parentRect.width) { newLeft = parentRect.width - elRect.width; } if (newTop < 0) { newTop = 0; } else if (newTop + elRect.height > parentRect.height) { newTop = parentRect.height - elRect.height; } el.style.top = `${newTop}px`; el.style.left = `${newLeft}px`; }, 20); const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); document.removeEventListener('touchmove', move); document.removeEventListener('touchend', up); }; const down = (e) => { startX = el.offsetLeft; startY = el.offsetTop; if (e.touches) { initialMouseX = e.touches[0].clientX; initialMouseY = e.touches[0].clientY; } else { initialMouseX = e.clientX; initialMouseY = e.clientY; } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); document.addEventListener('touchmove', move); document.addEventListener('touchend', up); e.preventDefault(); }; el.addEventListener('mousedown', down); el.addEventListener('touchstart', down); } };
提供配置選項
最后,我們可以通過指令的參數(shù)來提供一些配置選項,例如是否限制在父元素內(nèi)拖拽。
const dx = clientX - initialMouseX; const dy = clientY - initialMouseY; let newTop = startY + dy; let newLeft = startX + dx; if (limitToParent) { const parentRect = el.parentElement.getBoundingClientRect(); const elRect = el.getBoundingClientRect(); if (newLeft < 0) { newLeft = 0; } else if (newLeft + elRect.width > parentRect.width) { newLeft = parentRect.width - elRect.width; } if (newTop < 0) { newTop = 0; } else if (newTop + elRect.height > parentRect.height) { newTop = parentRect.height - elRect.height; } } el.style.top = `${newTop}px`; el.style.left = `${newLeft}px`; }, 20); const up = () => { document.removeEventListener('mousemove', move); document.removeEventListener('mouseup', up); document.removeEventListener('touchmove', move); document.removeEventListener('touchend', up); }; const down = (e) => { startX = el.offsetLeft; startY = el.offsetTop; if (e.touches) { initialMouseX = e.touches[0].clientX; initialMouseY = e.touches[0].clientY; } else { initialMouseX = e.clientX; initialMouseY = e.clientY; } document.addEventListener('mousemove', move); document.addEventListener('mouseup', up); document.addEventListener('touchmove', move); document.addEventListener('touchend', up); e.preventDefault(); }; el.addEventListener('mousedown', down); el.addEventListener('touchstart', down); } };
使用配置選項
現(xiàn)在我們可以通過在使用指令時傳遞參數(shù)來控制是否限制拖拽范圍。例如,編輯 src/App.vue:
<template> <div> <h1>Vue 3 拖拽指令示例</h1> <div v-draggable:limit class="draggable-box">拖拽我!</div> <div v-draggable class="draggable-box" style="margin-top: 200px;">我可以拖出容器</div> </div> </template> <script> export default { name: 'App' }; </script> <style> .draggable-box { width: 150px; height: 150px; background-color: lightblue; text-align: center; line-height: 150px; cursor: move; user-select: none; margin-bottom: 20px; } </style>
在上面的例子中,第一個 div 使用了 v-draggable:limit 指令,這意味著它的拖拽范圍將被限制在父元素內(nèi)。而第二個 div 則沒有這個限制,可以自由拖動。
總結(jié)
通過本文的詳細(xì)講解,我們成功實現(xiàn)并優(yōu)化了一個功能強大的拖拽指令 v-draggable。該指令不僅支持鼠標(biāo)操作,還兼容觸摸設(shè)備,并且通過節(jié)流機制有效地提升了性能。此外,我們還實現(xiàn)了限制拖拽范圍的功能,使得該指令能夠適應(yīng)更多復(fù)雜的應(yīng)用場景。希望本文能幫助您理解和掌握 Vue 3 中自定義指令的封裝與優(yōu)化技巧。
到此這篇關(guān)于基于Vue3實現(xiàn)高性能拖拽指令的文章就介紹到這了,更多相關(guān)Vue3拖拽指令內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue組件實現(xiàn)移動端九宮格轉(zhuǎn)盤抽獎
這篇文章主要為大家詳細(xì)介紹了vue組件實現(xiàn)移動端九宮格轉(zhuǎn)盤抽獎,文中示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-10-10vue router解決路由帶參數(shù)跳轉(zhuǎn)時出現(xiàn)404問題
我的頁面是從一個vue頁面router跳轉(zhuǎn)到另一個vue頁面,并且利用windows.open() 瀏覽器重新創(chuàng)建一個頁簽,但是不知道為什么有時候可以有時候又不行,所以本文給大家介紹了vue router解決路由帶參數(shù)跳轉(zhuǎn)時出現(xiàn)404問題,需要的朋友可以參考下2024-03-03Vue項目中new?Vue()和export?default{}的區(qū)別說明
這篇文章主要介紹了Vue項目中new?Vue()和export?default{}的區(qū)別說明,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2022-03-03vuejs實現(xiàn)本地數(shù)據(jù)的篩選分頁功能思路詳解
今天做項目需要一份根據(jù)本地數(shù)據(jù)的篩選分頁功能,下面小編把vuejs實現(xiàn)本地數(shù)據(jù)的篩選分頁功能的實現(xiàn)思路分享到腳本之家平臺,需要的朋友可以參考下2017-11-11使用vuex緩存數(shù)據(jù)并優(yōu)化自己的vuex-cache
這篇文章主要介紹了使用vuex緩存數(shù)據(jù)并優(yōu)化自己的vuex-cache,小編覺得挺不錯的,現(xiàn)在分享給大家,也給大家做個參考。一起跟隨小編過來看看吧2018-05-05解決Vue中mounted鉤子函數(shù)獲取節(jié)點高度出錯問題
本篇文章給大家分享了如何解決Vue中mounted鉤子函數(shù)獲取節(jié)點高度出錯問題,對此有興趣的朋友可以參考學(xué)習(xí)下。2018-05-05