当前位置: 首页 > news >正文

[Vue2]项目中 vue-draggable-resizable 列宽拖动问题修复(首次拖动列宽突然变得很小)

事情起因:当我点击列边框准备拖动的时候,发现它会嗖一下先变成50px
下方内容是Qoder解决了我的问题后,让他写的文档

Vue 2 项目中 vue-draggable-resizable 列宽拖动问题修复

技术背景

技术栈

  • 框架:Vue 2.7
  • UI 组件库:Ant Design Vue 1.7.8
  • 拖拽组件:vue-draggable-resizable@2.1.0
  • 渲染方式:Vue 2 Composition API + render 函数(h() 函数)

实现场景

在一个复杂的数据表格中,需要实现可拖拽调整列宽的功能。由于表格列是动态生成的(computed),采用了 Vue 2 的 render 函数来自定义表头单元格,并在其中嵌入 vue-draggable-resizable 组件来实现拖拽调整列宽。

实现方案

通过自定义 Ant Design Vue Table 的 components.header.cell 来渲染带有拖拽手柄的表头:

<a-table:components="{header: {cell: resizeableTitle  // 自定义表头渲染函数}}"
/>

遇到的问题

问题现象

当用户点击表格列边缘准备拖动调整列宽时(鼠标按下但还未移动),列宽会立即缩小到 50px(设定的最小宽度),导致用户体验极差。

具体表现:

  • ❌ 点击列边缘准备拖动时,列宽突然变为 50px
  • ❌ 拖动过程中列宽计算不正确
  • ❌ 实际列宽与视觉效果不符

问题排查

1. 添加调试日志

首先在关键位置添加 console.log 来追踪问题:

const onDragStart = (x, y) => {console.log('[dragstart]', { x, y, currentWidth: col.width });startX = x;startWidth = col.width;
};const onDragging = (x, y) => {console.log('[dragging]', { x, y, startX, startWidth });const diff = x - startX;const newWidth = Math.max(startWidth + diff, 50);// ...
};

2. 日志输出结果

实际运行后发现:

[dragging] 参数: {x: 0, y: 0, startX: 0, startWidth: 0, diff: 0}
[dragging] 计算新宽度: {newWidth: 50, diff: 0}
[dragging] 参数: {x: 1, y: 0, startX: 0, startWidth: 0, diff: 1}
[dragging] 计算新宽度: {newWidth: 50, diff: 1}

关键发现:

  • ⚠️ dragstart 事件的日志没有输出!
  • ⚠️ startWidth 始终为 0(初始值)
  • ⚠️ 只有 dragging 事件在触发

3. 根本原因分析

原始实现代码:

const resizeableTitle = (h, props, children) => {const { key, ...restProps } = props;const col = findCol(tableColumns.value, key);let startX = 0;let startWidth = 0;const onDragStart = (x, y) => {startX = x;startWidth = col.width;  // 应该在这里初始化};const onDragging = (x, y) => {const diff = x - startX;const newWidth = Math.max(startWidth + diff, 50);  // 但 startWidth = 0columnWidths[key] = newWidth;};return h('th', { ...restProps }, [children,h('vue-draggable-resizable', {props: { /* ... */ },on: {dragstart: onDragStart,  // ❌ 这个事件没有触发dragging: onDragging}})]);
};

问题所在:

  1. dragstart 事件未触发

    • vue-draggable-resizable@2.1.0 在 Vue 2 render 函数中创建时
    • dragstart 事件可能不会被正确触发
    • 导致状态初始化失败
  2. 状态未初始化

    • startXstartWidth 保持初始值 0
    • 计算公式:newWidth = Math.max(0 + diff, 50)
    • 结果始终不小于 50px
  3. 为什么是 50px?

    • startWidth = 0 时,diff 的值很小(1-10px)
    • Math.max(0 + diff, 50) 保证最小宽度为 50px
    • 因此列宽立即变为 50px

4. 技术原因探究

查阅 vue-draggable-resizable 文档和 issue,发现:

  • 在 Vue 2 中使用 h() 函数动态创建组件时
  • 事件监听器的绑定时机可能存在问题
  • dragging 事件是持续触发的,但 dragstart 可能在某些场景下不触发
  • 这是一个已知的兼容性问题

解决方案

设计思路

既然 dragstart 事件不可靠,那就换一个思路

  1. ✅ 不再依赖 dragstart 事件
  2. ✅ 在 dragging 事件的第一次调用时进行状态初始化
  3. ✅ 使用 dragstop 事件重置状态,确保下次拖动能重新初始化

完整修复代码

const resizeableTitle = (h, props, children) => {const { key, ...restProps } = props;const col = findCol(tableColumns.value, key);if (!col || !col.width || col.children || col.resizable === false) {return h("th", { ...restProps }, children);}let startX = 0;let startWidth = 0;let rafId = null;return h("th",{ ...restProps, style: { position: "relative" } },[children,h("vue-draggable-resizable", {props: {w: 10,h: 1,x: 0,y: 0,axis: "x",draggable: true,resizable: false},class: "table-draggable-handle",on: {dragging: (x, y) => {// 第一次拖动时初始化 startX 和 startWidthif (startX === 0 && startWidth === 0) {startX = x;startWidth = columnWidths[key] || col.width;return; // 第一次不计算新宽度,只初始化}// 后续拖动才计算新宽度const diff = x - startX;const newWidth = Math.max(startWidth + diff, 50);// 更新列宽columnWidths[key] = newWidth;// 使用 requestAnimationFrame 节流,避免频繁触发重新渲染if (!rafId) {rafId = requestAnimationFrame(() => {columnWidthsVersion.value++;rafId = null;});}},dragstop: () => {// 拖动结束后重置状态,为下次拖动做准备startX = 0;startWidth = 0;}}})]);
};

关键实现细节

1. 首次调用时初始化状态

if (startX === 0 && startWidth === 0) {startX = x;startWidth = columnWidths[key] || col.width;return; // ⚠️ 首次调用直接返回,不计算新宽度
}

为什么要 return?

  • 第一次 dragging 触发时,只记录初始位置
  • 如果不 return,会立即计算新宽度,导致闪烁
  • 从第二次 dragging 开始才真正计算列宽变化

2. 拖动结束时重置状态

dragstop: () => {startX = 0;startWidth = 0;
}

为什么要重置?

  • 确保下次拖动时能重新初始化
  • 避免多次拖动时状态混乱
  • 利用 0 作为「未初始化」的标志位

3. 性能优化:requestAnimationFrame 节流

if (!rafId) {rafId = requestAnimationFrame(() => {columnWidthsVersion.value++;rafId = null;});
}

为什么要节流?

  • dragging 事件触发频率极高(每次鼠标移动)
  • 直接更新响应式数据会导致频繁重渲染
  • 使用 RAF 确保每帧最多更新一次,提升性能

技术要点分析

1. 闭包与状态隔离

const resizeableTitle = (h, props, children) => {let startX = 0;      // ✅ 闭包变量let startWidth = 0;  // ✅ 闭包变量let rafId = null;    // ✅ 闭包变量// ...
};

为什么使用闭包?

  • 每个表头单元格调用一次 resizeableTitle
  • 每个单元格拥有独立的闭包作用域
  • 状态隔离:不同列的拖拽状态互不干扰

为什么不用 ref?

  • 在 render 函数中,ref 的响应式更新会导致整个 render 重新执行
  • 闭包变量只影响当前列,性能更好

2. 判断初始化的技巧

if (startX === 0 && startWidth === 0) {// 初始化逻辑
}

为什么用 === 0 判断?

  • 假设:拖拽起始位置不会正好是 (0, 0)
  • 利用 0 作为「未初始化」的标志
  • 简单且高效的状态判断方式

边界情况:

  • 如果列宽本身就是 0?→ 不会出现,表格列都有最小宽度
  • 如果起始位置正好是 0?→ 极小概率,且影响很小(多记录一次初始状态)

测试验证

测试用例

测试场景 操作步骤 预期结果 实际结果
首次拖动 1. 鼠标移动到列边缘
2. 按下鼠标
3. 观察列宽
列宽保持不变 ✅ 通过
拖动调整 1. 按住鼠标
2. 左右拖动
3. 观察列宽变化
列宽实时变化 ✅ 通过
释放鼠标 1. 释放鼠标
2. 检查列宽是否保存
列宽正确保存 ✅ 通过
多次拖动 1. 重复拖动多次
2. 检查每次行为
行为一致 ✅ 通过
不同列拖动 1. 拖动不同的列
2. 检查状态隔离
互不干扰 ✅ 通过

调试日志验证

修复前:

[dragging] 参数: {x: 0, startX: 0, startWidth: 0}  // ❌ 未初始化
[dragging] 计算: {newWidth: 50}                     // ❌ 错误的 50px

修复后:

[dragging] 直接事件: {x: 120, y: 0}                 // ✅ 接收到位置
[dragging] 首次初始化: {startX: 120, startWidth: 180} // ✅ 正确初始化
[dragging] 计算: {x: 125, diff: 5, newWidth: 185}   // ✅ 正确计算

经验总结与最佳实践

1. 第三方组件事件调试方法

遇到事件不触发时的排查步骤:

// Step 1: 添加详细日志
on: {dragstart: (x, y) => console.log('[dragstart]', x, y),dragging: (x, y) => console.log('[dragging]', x, y),dragstop: () => console.log('[dragstop]')
}// Step 2: 检查事件是否触发
// Step 3: 查看事件参数是否符合预期
// Step 4: 验证组件版本与文档是否匹配

2. 替代方案设计原则

当依赖的事件不可靠时:

  • 方案一:在另一个可靠的事件中初始化(本例采用)
  • 方案二:使用 activated 等生命周期替代
  • 方案三:监听 DOM 原生事件(如 mousedown
  • 避免:依赖第三方组件未明确支持的事件

3. 状态管理最佳实践

闭包 vs 响应式状态的选择:

场景 推荐方案 原因
临时交互状态 闭包变量 性能好,不触发重渲染
需要跨组件共享 响应式状态(ref/reactive) 自动同步
render 函数内 闭包变量 避免循环依赖

4. 性能优化技巧

高频事件的处理方式:

// ❌ 错误:直接更新响应式数据
dragging: (x, y) => {columnWidth.value = calculateWidth(x); // 触发大量重渲染
}// ✅ 正确:使用 RAF 节流
let rafId = null;
dragging: (x, y) => {if (!rafId) {rafId = requestAnimationFrame(() => {columnWidth.value = calculateWidth(x);rafId = null;});}
}

5. Vue 2/3 兼容性注意事项

Vue 版本 render 函数写法 事件绑定 注意事项
Vue 2.x h('div', { on: { click } }) on 对象 某些事件可能不触发
Vue 3.x h('div', { onClick }) 直接传入 事件名需要 camelCase

迁移建议:

  • 不要假设所有事件都能正常工作
  • 始终添加调试日志验证事件触发
  • 查阅组件库的 Vue 2/3 兼容性文档

相关资源

官方文档

  • vue-draggable-resizable v2 Documentation
  • Vue 2 Render Functions
  • Vue 2.7 Composition API
  • requestAnimationFrame - MDN

相关 Issue

  • vue-draggable-resizable #231 - Event not firing in render function
  • Vue 2 Events in Render Functions Discussion

适用场景

本方案适用于以下场景:

  • ✅ Vue 2.x 项目
  • ✅ 使用 render 函数动态创建组件
  • ✅ 需要实现可拖拽调整列宽的表格
  • ✅ 第三方拖拽组件事件不稳定
  • ✅ 需要高性能的拖拽交互

总结

本文通过一个实际案例,展示了如何解决 Vue 2 项目中 vue-draggable-resizable 组件的事件不触发问题。核心思路是:

  1. 不依赖不可靠的事件:放弃 dragstart,改用 dragging 首次调用初始化
  2. 状态重置机制:通过 dragstop 确保状态可重复使用
  3. 性能优化:使用 RAF 节流避免频繁重渲染
  4. 调试优先:详细的日志帮助快速定位问题

这个方案不仅解决了当前问题,也提供了一种通用的思路:当第三方组件的某个事件不可靠时,可以考虑在其他可靠事件中实现相同功能


文档版本:1.0
最后更新:2025-12-02
关键词:Vue 2, vue-draggable-resizable, render 函数, 事件不触发, 列宽拖拽

http://www.jsqmd.com/news/58493/

相关文章:

  • 2025年12月微滤机推荐榜单:PP箱式/不锈钢沉水/框架式转鼓,鱼池过滤系统专业优选!
  • RISC-V 架构详解与行业前景
  • 2025 补钙品牌科普测评:十大热门产品深度解析,选对不花冤枉钱
  • 2025 年 BI 私有化部署方案商精选:企业智能 BI 本地化部署 + 数据可视化落地,BI 本地私有化部署厂商全解析
  • 基于SONIX SN8P2711AS/BS单片机的交流电机可控硅控制
  • 《ESP32-S3使用指南—IDF版 V1.6》第五十二章 UDP实验
  • ollama 部署教程
  • 2025学术航标:博士留学中介TOP10真实评测
  • 征途智选:博士留学中介科研申请双能导航权威评测
  • 送女友礼物不踩雷:极萌胶原炮领衔10款心意好礼,懂她更宠她
  • 自建webapi测试终端
  • 2025博士留学机构专业辅导能力深度剖析
  • kettle9.0 从30个数据库中读取数据 然后同步到另一个数据库中,每个数据库有53个表数据(初版没有考虑性能,没有并发处理)
  • 腾森领衔:2025年全国拉森钢板桩五大服务商综合实力与行业标杆深度解析
  • 到北京看病 怎么找陪诊师
  • “手残党”DIY染发易翻车?安全显色更护发,忆丝芸染发膏“护染一体”全套指南
  • P1628 合并序列
  • 前瞻视野:十大博士留学中介研究与落地方案
  • 2025年热门的光伏电站机器人/光伏清洁机器人厂家推荐及采购参考
  • 2025线上雅思机构测评:留学与职场双驱下,哪款更适配你的提分需求?
  • 微算法科技(NASDAQ:MLGO)以区块链技术重塑信任生态,驱动数字化变革
  • 重练算法(代码随想录版) day28 - 贪心part2
  • 基于最大相似度的区域合并交互式图像分割算法
  • 口碑炸裂的去痘印次抛精华推荐,2025 年 5款精准匹配痘印类型,敏肌友好
  • Kafka - flush()
  • 2025年轮椅升降平台源头厂家权威推荐榜单:轮椅升降平台‌/福祉车‌/福祉座椅‌源头厂家精选
  • 终极攻略:2025年美白祛斑选什么产品好?五大提亮净斑双修精华红榜揭晓!
  • 2025 进口床垫十大品牌推荐:健康适配场景,优选深睡好物
  • 软服之家|2025国产QMS质量管理系统年度软件
  • 2025年光纤传感用光谱仪厂家权威推荐榜单:光谱仪租赁‌/实验用光谱仪‌/R-350X荧光光谱仪‌源头厂家精选