如何在 React 中实现一个可拖动的组件,利用react - draggable库或者原生事件实现的思路分别是什么?

大白话 如何在 React 中实现一个可拖动的组件,利用react - draggable库或者原生事件实现的思路分别是什么?

一、当产品说“加个拖拽功能”

凌晨三点,工位上的台灯还亮着。看着产品经理新提的需求——“这个卡片要能拖到任意位置”,你不禁扶额叹息:上次用原生事件写拖拽,被边界bug搞到脱发;这次用库吧,又怕面试官问原理答不上来……

别急!今天咱们就来盘一盘React中实现可拖动组件的两种核心方案第三方库快速落地 vs 原生事件深度定制。不仅能解决当下需求,还能让你在面试时从原理到代码对答如流,文末还有面试保命口诀性能优化技巧,坐稳了,发车!

二、拖拽的“三板斧”事件原理

先抛开框架,想想拖拽的本质是什么?其实就三个阶段的事件联动:

1. 按下鼠标(touchstart):锚定起点

  • 记录鼠标点击时的初始坐标(clientX/clientY)
  • 记录拖拽元素的初始位置(left/top)
  • 关键操作:阻止浏览器默认行为(e.preventDefault()),否则会选中文字

2. 移动鼠标(touchmove):实时计算位置

  • 鼠标移动时,用当前坐标 - 初始偏移量算出新位置
  • 注意!必须在document级别监听移动事件,否则鼠标移出元素时事件会丢失

3. 松开鼠标(touchend):结束拖拽

  • 移除全局事件监听,避免内存泄漏
  • 触发拖拽结束回调,比如保存位置到本地存储

灵魂拷问:为什么移动端还要处理touch事件?因为手机没有鼠标,拖拽依赖手指触摸,需要用touchstart/touchmove/touchend,并且要兼容两种事件系统(鼠标和触摸)。

三、方案一:用react-draggable库5分钟快速上线

适合场景:需求紧急、非复杂交互、需要兼容移动端

代码示例(带详细注释):

import React from 'react';
// 引入拖拽库(npm install react-draggable --save)
import Draggable from 'react-draggable'; 
// 引入样式(记得自己写对应的CSS哦)
import './DraggableBox.css';

function DraggableCard() {
  // 拖拽开始时记录时间,用于计算拖拽速度(扩展功能)
  const [dragStartTime, setDragStartTime] = useState(0);
  // 拖拽结束时打印位置(面试常问:如何获取拖拽后的坐标?)
  const handleDragStop = (e, data) => {
    const dragDuration = Date.now() - dragStartTime;
    console.log(`拖拽结束!新位置:X ${data.x}px,Y ${data.y}px`);
    console.log(`拖拽耗时:${dragDuration}ms`);
  };

  // 拖拽开始时记录时间戳
  const handleDragStart = () => {
    setDragStartTime(Date.now());
  };

  return (
    <div className="page-container">
      <h3>用react-draggable实现的可拖拽卡片</h3>
      <Draggable
        // 指定拖拽手柄:只有点击这个区域才能拖拽
        handle=".drag-handle" 
        // 禁止拖拽区域:点击这里不会触发拖拽
        cancel=".no-drag" 
        // 拖拽开始回调(可选)
        onStart={handleDragStart} 
        // 拖拽结束回调(必选)
        onStop={handleDragStop} 
        // 限制拖拽范围:卡片不能拖出父容器
        bounds=".page-container" 
      >
        <div className="draggable-card">
          {/* 拖拽手柄区域 */}
          <div className="drag-handle">📌 按住我拖动</div>
          {/* 禁止拖拽区域 */}
          <div className="no-drag">❌ 这里不能拖</div>
          <p>当前坐标:X {Math.round(props.x)}px,Y {Math.round(props.y)}px</p>
        </div>
      </Draggable>
    </div>
  );
}

export default DraggableCard;

核心原理(面试要这么说):

这个库帮我们做了三件事:

  1. 封装事件系统:自动处理鼠标和触摸事件的兼容
  2. 边界限制:通过bounds属性限制拖拽范围(原理是计算新位置是否超出父容器)
  3. 性能优化:使用requestAnimationFrame更新位置,避免卡顿

四、方案二:纯原生事件实现(面试官最爱问的硬核方案)

适合场景:需要高度定制、性能要求极高、禁止引入第三方库

代码示例(完整可运行,含触摸事件兼容):

import React, { useState, useRef, useEffect } from 'react';
import './NativeDraggable.css';

function NativeDraggable() {
  // 存储位置状态(用CSS translate优化性能,比left/top更好)
  const [transform, setTransform] = useState('translate(0px, 0px)');
  // 记录初始偏移量(鼠标点击位置 - 元素位置)
  const [offset, setOffset] = useState({ x: 0, y: 0 });
  // 是否正在拖拽(防止松开后误触发移动事件)
  const [isDragging, setIsDragging] = useState(false);
  // 引用DOM元素(用于获取初始位置)
  const dragRef = useRef(null);

  // 鼠标按下事件(拖拽起点)
  const handleMouseDown = (e) => {
    if (e.button !== 0) return; // 只处理左键
    e.preventDefault(); // 阻止选中文字
    setIsDragging(true); // 标记为拖拽状态

    // 获取元素当前位置(用getBoundingClientRect更精准)
    const rect = dragRef.current.getBoundingClientRect();
    // 计算鼠标点击位置相对于元素的偏移量
    setOffset({
      x: e.clientX - rect.left,
      y: e.clientY - rect.top
    });

    // 绑定全局移动和松开事件(必须在document上,否则鼠标移出会丢失)
    document.addEventListener('mousemove', handleMouseMove);
    document.addEventListener('mouseup', handleMouseUp);
  };

  // 触摸开始事件(移动端兼容)
  const handleTouchStart = (e) => {
    if (e.touches.length !== 1) return; // 只处理单指触摸
    const touch = e.touches[0];
    handleMouseDown({ // 复用鼠标逻辑,统一处理
      clientX: touch.clientX,
      clientY: touch.clientY,
      preventDefault: () => e.preventDefault()
    });
  };

  // 鼠标移动事件(实时更新位置)
  const handleMouseMove = (e) => {
    if (!isDragging) return; // 非拖拽状态不处理

    // 计算新位置:当前坐标 - 偏移量 = 元素左上角坐标
    const newX = e.clientX - offset.x;
    const newY = e.clientY - offset.y;
    
    // 用transform更新位置(性能优于left/top)
    setTransform(`translate(${newX}px, ${newY}px)`);
  };

  // 鼠标松开事件(结束拖拽)
  const handleMouseUp = () => {
    setIsDragging(false); // 结束拖拽状态
    // 移除全局事件监听(重要!防止内存泄漏)
    document.removeEventListener('mousemove', handleMouseMove);
    document.removeEventListener('mouseup', handleMouseUp);
  };

  // 触摸结束事件(移动端兼容)
  const handleTouchEnd = () => {
    handleMouseUp(); // 复用鼠标逻辑
  };

  return (
    <div 
      ref={dragRef} 
      className="native-draggable"
      // 绑定鼠标事件
      onMouseDown={handleMouseDown}
      // 绑定触摸事件
      onTouchStart={handleTouchStart}
      onTouchEnd={handleTouchEnd}
    >
      <h3>🚀 原生实现可拖拽方块</h3>
      <p>按住任意位置拖动</p>
    </div>
  );
}

export default NativeDraggable;

关键优化点(面试加分项):

  1. 使用transform替代left/top:因为transform不会触发浏览器重排,性能更好
  2. 统一处理鼠标和触摸事件:通过复用逻辑减少代码冗余
  3. 边界限制实现(扩展代码):
// 在handleMouseMove中添加边界限制
const maxX = window.innerWidth - rect.width; // 最大X坐标
const maxY = window.innerHeight - rect.height; // 最大Y坐标
const newX = Math.max(0, Math.min(e.clientX - offset.x, maxX));
const newY = Math.max(0, Math.min(e.clientY - offset.y, maxY));

五、选库还是手写?一看就懂

维度react-draggable(库方案)原生事件(手写方案)
开发效率✅ 5分钟上线❌ 至少2小时调试
代码量✅ 30行代码❌ 150行+(含兼容和优化)
定制性❌ 受限于库API✅ 可实现任意逻辑(如吸附效果)
性能⚪ 中等(库有少量封装开销)⚫ 高性能(直接操作DOM)
面试考点❓ 考库的原理和配置❗ 必考原生事件流程
适合场景快速开发、后台管理系统复杂交互、可视化编辑器
移动端兼容✅ 内置触摸处理❌ 需要手动实现touch事件

六、面试保命指南:3句话让面试官点头

场景1:“说说用react-draggable的实现思路”

回答模板
“首先安装库,然后用Draggable组件包裹目标元素,通过handle指定拖拽手柄,用onStop获取结束位置。库内部会自动处理鼠标和触摸事件,还能通过bounds限制拖拽范围,适合快速实现基础功能。”

场景2:“原生实现拖拽的核心步骤是什么?”

回答模板
“分三步:

  1. mousedown时记录初始偏移量,绑定全局mousemove和mouseup事件;
  2. mousemove时用当前坐标减去偏移量计算新位置,用transform更新;
  3. mouseup时移除全局事件,结束拖拽。移动端需要额外处理touch事件,逻辑和鼠标类似。”

场景3:“两种方案的优缺点是什么?”

回答模板
“用库的优点是快,但定制性差;原生的优点是灵活,但要自己处理兼容性和性能问题。项目中如果时间紧就用库,需要高性能或特殊效果就手写。”

七、扩展思考:从拖拽到全链路优化

1. 如何实现拖拽排序(如todo列表)?

核心思路:

  • 拖拽时获取元素索引,移动时交换数组位置
  • 推荐库:react-beautiful-dnd(专门做排序,内置动画)

2. 拖拽时卡顿怎么办?

优化技巧:

  • requestAnimationFrame批量更新位置
  • 将元素设置为will-change: transform
  • 避免在拖拽过程中触发重绘(如操作其他DOM)

3. 如何实现吸附效果(如靠近边缘自动对齐)?

关键代码:

// 在handleMouseMove中添加吸附逻辑
const吸附距离 = 50; // 距离边缘50px时触发
const newX = e.clientX - offset.x;
if (newX < 吸附距离) setTransformX(0);
else if (newX > window.innerWidth - width - 吸附距离) setTransformX(maxX);

八、总结:成年人不做选择,两种方案我都要

  • 紧急需求:直接上react-draggable,记得看文档学透boundsaxis配置
  • 面试准备:必须手写原生逻辑,搞懂事件冒泡和性能优化点
  • 终极建议:平时积累组件库用法,面试前手写一遍原生代码,两者结合才是真·高手

下次我们聊聊“如何用React实现一个高性能的虚拟列表”,记得关注哦!夜深了,写完代码记得给自己泡杯咖啡,毕竟——前端路漫漫,代码是伙伴,秃头不可怕,原理要搞懂!

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

前端布洛芬

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值