react实现框选功能
实现框选功能的基本思路
框选功能通常涉及监听鼠标事件、绘制选框以及处理选框内的元素。React中可以通过结合原生DOM事件和状态管理来实现。
监听鼠标事件
在组件挂载时添加mousedown、mousemove和mouseup事件监听器,卸载时移除这些监听器。使用useEffect钩子来处理这些副作用。

useEffect(() => {
const handleMouseDown = (e) => {
// 记录起始坐标
};
const handleMouseMove = (e) => {
// 计算选框尺寸并绘制
};
const handleMouseUp = (e) => {
// 处理选框内的元素
};
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, []);
绘制选框
使用useState存储选框的坐标和尺寸。在鼠标移动时更新这些值,并通过CSS绝对定位和transform来绘制选框。

const [selectionBox, setSelectionBox] = useState({
startX: 0,
startY: 0,
width: 0,
height: 0,
isSelecting: false,
});
// 在handleMouseMove中更新选框
setSelectionBox({
startX: Math.min(startX, currentX),
startY: Math.min(startY, currentY),
width: Math.abs(currentX - startX),
height: Math.abs(currentY - startY),
isSelecting: true,
});
处理选框内元素
使用Element.getBoundingClientRect()获取每个可选元素的边界框,检查是否与选框相交。可以通过intersects函数来判断两个矩形是否重叠。
const intersects = (rect1, rect2) => {
return !(
rect2.left > rect1.right ||
rect2.right < rect1.left ||
rect2.top > rect1.bottom ||
rect2.bottom < rect1.top
);
};
// 在handleMouseUp中筛选元素
const selectedElements = elements.filter(element => {
const elementRect = element.getBoundingClientRect();
const selectionRect = {
left: selectionBox.startX,
top: selectionBox.startY,
right: selectionBox.startX + selectionBox.width,
bottom: selectionBox.startY + selectionBox.height,
};
return intersects(elementRect, selectionRect);
});
完整组件示例
import React, { useState, useEffect, useRef } from 'react';
const SelectionBox = () => {
const [selectionBox, setSelectionBox] = useState({
startX: 0,
startY: 0,
width: 0,
height: 0,
isSelecting: false,
});
const [selectedItems, setSelectedItems] = useState([]);
const itemsRef = useRef([]);
useEffect(() => {
const handleMouseDown = (e) => {
setSelectionBox({
startX: e.clientX,
startY: e.clientY,
width: 0,
height: 0,
isSelecting: true,
});
};
const handleMouseMove = (e) => {
if (!selectionBox.isSelecting) return;
setSelectionBox(prev => ({
...prev,
width: e.clientX - prev.startX,
height: e.clientY - prev.startY,
}));
};
const handleMouseUp = () => {
if (!selectionBox.isSelecting) return;
const selectionRect = {
left: selectionBox.width > 0
? selectionBox.startX
: selectionBox.startX + selectionBox.width,
top: selectionBox.height > 0
? selectionBox.startY
: selectionBox.startY + selectionBox.height,
right: selectionBox.width > 0
? selectionBox.startX + selectionBox.width
: selectionBox.startX,
bottom: selectionBox.height > 0
? selectionBox.startY + selectionBox.height
: selectionBox.startY,
};
const selected = itemsRef.current.filter(item => {
const itemRect = item.getBoundingClientRect();
return !(
selectionRect.right < itemRect.left ||
selectionRect.left > itemRect.right ||
selectionRect.bottom < itemRect.top ||
selectionRect.top > itemRect.bottom
);
});
setSelectedItems(selected.map(el => el.id));
setSelectionBox({
startX: 0,
startY: 0,
width: 0,
height: 0,
isSelecting: false,
});
};
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
return () => {
document.removeEventListener('mousedown', handleMouseDown);
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
};
}, [selectionBox.isSelecting]);
return (
<div style={{ position: 'relative', height: '100vh' }}>
{selectionBox.isSelecting && (
<div
style={{
position: 'absolute',
left: selectionBox.width > 0
? selectionBox.startX
: selectionBox.startX + selectionBox.width,
top: selectionBox.height > 0
? selectionBox.startY
: selectionBox.startY + selectionBox.height,
width: Math.abs(selectionBox.width),
height: Math.abs(selectionBox.height),
backgroundColor: 'rgba(0, 0, 255, 0.2)',
border: '1px solid blue',
}}
/>
)}
{[1, 2, 3, 4, 5].map(id => (
<div
key={id}
ref={el => itemsRef.current[id] = el}
style={{
position: 'absolute',
left: `${id * 100}px`,
top: `${id * 100}px`,
width: '50px',
height: '50px',
backgroundColor: selectedItems.includes(id)
? 'red'
: 'gray',
}}
/>
))}
</div>
);
};
export default SelectionBox;
性能优化建议
对于大量可选项的场景,可以考虑以下优化:
- 使用虚拟滚动只渲染可视区域内的元素
- 对选框检测使用四叉树等空间分区技术
- 节流鼠标移动事件处理
注意事项
- 确保选框坐标计算考虑了页面滚动位置
- 处理鼠标事件时注意阻止默认行为以避免冲突
- 在严格模式下注意事件监听器的清理






