[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 来渲染带有拖拽手柄的表头:

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

遇到的问题

问题现象

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

具体表现:

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

问题排查

1. 添加调试日志

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

javascript 复制代码
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. 日志输出结果

实际运行后发现:

javascript 复制代码
[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. 根本原因分析

原始实现代码:

javascript 复制代码
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 = 0
    columnWidths[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 事件重置状态,确保下次拖动能重新初始化

完整修复代码

javascript 复制代码
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 和 startWidth
            if (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. 首次调用时初始化状态
javascript 复制代码
if (startX === 0 && startWidth === 0) {
  startX = x;
  startWidth = columnWidths[key] || col.width;
  return; // ⚠️ 首次调用直接返回,不计算新宽度
}

为什么要 return?

  • 第一次 dragging 触发时,只记录初始位置
  • 如果不 return,会立即计算新宽度,导致闪烁
  • 从第二次 dragging 开始才真正计算列宽变化
2. 拖动结束时重置状态
javascript 复制代码
dragstop: () => {
  startX = 0;
  startWidth = 0;
}

为什么要重置?

  • 确保下次拖动时能重新初始化
  • 避免多次拖动时状态混乱
  • 利用 0 作为「未初始化」的标志位
3. 性能优化:requestAnimationFrame 节流
javascript 复制代码
if (!rafId) {
  rafId = requestAnimationFrame(() => {
    columnWidthsVersion.value++;
    rafId = null;
  });
}

为什么要节流?

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

技术要点分析

1. 闭包与状态隔离

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

为什么使用闭包?

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

为什么不用 ref?

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

2. 判断初始化的技巧

javascript 复制代码
if (startX === 0 && startWidth === 0) {
  // 初始化逻辑
}

为什么用 === 0 判断?

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

边界情况:

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

测试验证

测试用例

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

调试日志验证

修复前:

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

修复后:

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

经验总结与最佳实践

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

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

javascript 复制代码
// 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. 性能优化技巧

高频事件的处理方式:

javascript 复制代码
// ❌ 错误:直接更新响应式数据
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 兼容性文档

相关资源

官方文档

相关 Issue

适用场景

本方案适用于以下场景:

  • ✅ 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 函数, 事件不触发, 列宽拖拽

相关推荐
带带弟弟学爬虫__1 小时前
ks安卓—did注册
前端·javascript·vue.js·python·网络爬虫
sztian681 小时前
JavaScript-----本地存储、数组中map方法、数组中join方法
开发语言·javascript·ecmascript
维维酱1 小时前
使用 TRAE SOLO: 搭建前端项目脚手架
前端
徐小夕@趣谈前端1 小时前
LuckyFlow:用Vue3实现的一款AI可视化工作流编辑器
vue.js·人工智能·编辑器
南山安1 小时前
JS 进阶:手写 instanceof 与JS继承全面讲解
javascript·面试·编程语言
恋猫de小郭1 小时前
Android Studio Otter 2 Feature 发布,最值得更新的 Android Studio
android·前端·flutter
小旭@2 小时前
vue3官方文档巩固
前端·javascript·vue.js
努力往上爬de蜗牛2 小时前
electron 打包
前端·javascript·electron
美自坚韧2 小时前
qiankun微前端
前端·vue.js