基于Vue3实现高性能拖拽指令

 更新时间:2024年11月28日 08:58:04   作者:乐闻x  
在现代前端开发中,拖拽功能是增强用户体验的重要手段之一,本文将详细介绍如何在Vue3中封装一个拖拽指令并通过实战例子演示其实现过程,希望对大家有所帮助

前言

在现代前端开发中,拖拽功能是增强用户体验的重要手段之一。本文将详细介绍如何在 Vue 3 中封装一个拖拽指令(v-draggable),并通过实战例子演示其实现过程。通过这篇教程,您将不仅掌握基础的拖拽功能,还能了解如何优化指令以提升其性能和灵活性,从而为您的项目增色。

封装拖拽指令思路

我们将封装一个简单的拖拽指令,名为 v-draggable,它允许我们在任何元素上添加拖拽功能。

指令逻辑

1.监听鼠标事件:我们需要监听 mousedown、mousemove 和 mouseup 事件。

2.计算拖动位置:根据鼠标移动的距离更新元素的位置。

3.清理事件:在拖动结束后移除事件监听器。

实现步骤

第一步:创建指令文件

在 src 目录下创建一个名为 directives 的文件夹,并在其中创建一个 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');

第三步:使用指令

现在我们可以在任何组件中使用这个拖拽指令。编辑 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>

优化拖拽指令

当前的拖拽指令已经可以基本实现拖拽功能了,但还有一些细节需要优化,例如:

1.限制拖拽范围

2.支持触摸设备

3.添加节流来优化性能

4.提供一些配置选项

限制拖拽范围

我们可以通过对元素的位置进行限制,来防止其被拖出指定的范围。这里我们假定限制在父元素内进行拖拽。

// 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;

      // 限制拖拽范围在父元素内
      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();
    });
  }
};

支持触摸设备

为了支持触摸设备,我们需要添加 touchstart、touchmove 和 touchend 事件监听器。

// 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);
  }
};

添加节流优化性能

为了防止 mousemove 和 touchmove 事件触发得太频繁,我们可以使用节流(throttle)技术来优化性能。

// 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);
  }
};

提供配置选项

最后,我们可以通过指令的参数来提供一些配置选项,例如是否限制在父元素内拖拽。

      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);
  }
};

使用配置选项

现在我们可以通过在使用指令时传递参数来控制是否限制拖拽范围。例如,编辑 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 指令,这意味着它的拖拽范围将被限制在父元素内。而第二个 div 则没有这个限制,可以自由拖动。

总结

通过本文的详细讲解,我们成功实现并优化了一个功能强大的拖拽指令 v-draggable。该指令不仅支持鼠标操作,还兼容触摸设备,并且通过节流机制有效地提升了性能。此外,我们还实现了限制拖拽范围的功能,使得该指令能够适应更多复杂的应用场景。希望本文能帮助您理解和掌握 Vue 3 中自定义指令的封装与优化技巧。

到此这篇关于基于Vue3实现高性能拖拽指令的文章就介绍到这了,更多相关Vue3拖拽指令内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • vuex页面刷新数据丢失问题的四种解决方式

    vuex页面刷新数据丢失问题的四种解决方式

    vuex是大家使用vue时大多数都会选择的,但是当页面刷新之后vuex数据会丢失,下面这篇文章主要给大家介绍了关于vuex页面刷新数据丢失问题的四种解决方式,需要的朋友可以参考下
    2022-02-02
  • vue v-on:click传递动态参数的步骤

    vue v-on:click传递动态参数的步骤

    这篇文章主要介绍了vue v-on:click传递动态参数的步骤,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-09-09
  • vue中引用文件路径问题小结

    vue中引用文件路径问题小结

    vue路径分为绝对路径、相对路径、~+路径 及 别名+路径,在js中,引入带别名的文件路径,不需要在别名前加~ ,在css或者style中引入的需要在路径前面加~,路径以 ~ 开头,其后的部分将会被看作模块依赖,本文给大家介绍vue中引用文件路径问题,感兴趣的朋友一起看看吧
    2023-12-12
  • vue-cli中设置publicPath的几种方式对比

    vue-cli中设置publicPath的几种方式对比

    这篇文章主要介绍了vue-cli中设置publicPath的几种方式对比,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2022-07-07
  • vue中__ob__: Observer的踩坑记录

    vue中__ob__: Observer的踩坑记录

    这篇文章主要介绍了vue中__ob__: Observer的踩坑记录,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-10-10
  • vue路由实现登录拦截

    vue路由实现登录拦截

    这篇文章主要介绍了vue路由如何实现登录拦截,帮助大家更好的理解和学习使用vue框架,感兴趣的朋友可以了解下
    2021-03-03
  • 从零撸一个pc端vue的ui组件库( 计数器组件 )

    从零撸一个pc端vue的ui组件库( 计数器组件 )

    这篇文章主要介绍了pc端vue的ui组件库的实现方法,非常不错,具有一定的参考借鉴价值,需要的朋友可以参考下
    2019-08-08
  • Vue.js实现列表清单的操作方法

    Vue.js实现列表清单的操作方法

    Vue.js在设计上采用MVVM模式,当View视图层发生变化时,会自动更新到ViewModel.接下来通过本文给大家分享Vue.js实现列表清单的操作方法,需要的朋友参考下吧
    2017-11-11
  • 利用Vue Native构建移动应用的全过程记录

    利用Vue Native构建移动应用的全过程记录

    VueNative是一个使用JavaScript构建跨平台原生移动应用程序的框架m这篇文章主要给大家介绍了关于如何利用Vue Native构建移动应用的相关资料,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2021-08-08
  • 简单聊一聊Vue3组件更新过程

    简单聊一聊Vue3组件更新过程

    我们不光要学会Vue的组件化实现过程,还要懂得组件数据发生变化,更新组件的过程,这篇文章主要给大家介绍了关于Vue3组件更新过程的相关资料,需要的朋友可以参考下
    2022-04-04

最新评论