移动端拖拽排序不能用 dragstart 是因 iOS Safari 和 Android Chrome 默认禁用其触摸触发,需手动用 touchstart/touchmove/touchend 模拟,并用 transform、防抖、数据驱动更新等方案保障流畅与状态正确。
触摸设备上
拖拽排序
为什么不能直接用
?
因为移动端浏览器(尤其是 iOS Safari 和 Android Chrome)默认禁用
/
等原生拖放事件的触摸触发——这不是 bug,是规范行为。你绑了
,手指一按就毫无反应,控制台也不报错,纯静默失效。
必须绕过原生 drag API,改用
→
→
手动模拟拖拽逻辑。
禁用默认滚动:在
里调用
,否则手指一滑页面就滚走了
记录初始位置和目标元素索引:用
或数组下标存当前项原始位置
动态插入占位符(
)代替“被拖走”的视觉空缺,否则列表会突然塌陷
避免 touchmove 频繁重排:只在位移超过 10px 后才触发位置更新,防抖用
包一层更稳
如何让拖拽项跟着手指实时移动(不卡顿)?
关键不是绝对定位 + left/top,而是用
。iOS Safari 对 transform 的渲染走 GPU,比改 top/left 流畅得多,且不会触发 layout。
示例核心逻辑:
立即学习
“
前端免费学习笔记(深入)
”;
使用HTML,CSS,JavaScript开发Android应用程序 英文文字pdf版附源文件
如果你了解HTML,CSS和JavaScript,您已经拥有所需的工具开发Android应用程序。本动手本书展示了如何使用这些开源web标准设计和建造,可适应任何Android设备的应用程序 - 无需使用Java。您将学习如何创建一个在您选择的平台的Android友好的网络应用程序,然后转换与自由PhoneGap框架到一个原生的Android应用程序。了解为什么设备无关的移动应用是未来的潮流,并开始构建应用程序,提供更
下载
排序后如何正确更新 DOM 并保持状态?
别直接
或
移动真实 DOM 节点——这会导致 input 失焦、video 暂停、canvas 清空等副作用。真实项目里,拖拽只是“改顺序”,渲染应由框架或数据驱动重新生成。
维护一个有序的
数组,拖拽结束时只交换数组内两个元素(用 splice + index 计算)
用
(如
)绑定每个项,确保 diff 时复用节点而非销毁重建
如果不用框架,手动重排需先 detach 所有子节点,再按新顺序 append,避免 layout thrashing
特别注意表单控件:拖拽中若某项含
,要提前保存其
和
,重排后恢复
iOS 上 touchmove 有时不触发?
这是 iOS 15.4+ 的已知问题:当页面有
的父容器,且 touchstart 在该容器内触发时,后续 touchmove 可能被吞掉。根本原因是 Safari 的“scroll cancellation”机制误判为滚动意图。
解决办法只有两个有效:
给触发拖拽的容器加
(但会禁用内部所有滚动,慎用)
更稳妥的是:检测到
后立即在
上临时加
,
后立刻恢复为
不要依赖
,iOS 对 passive 选项处理不一致,
才是唯一可靠拦截方式
真正麻烦的从来不是拖拽本身,而是拖拽过程中用户突然缩放、横竖屏切换、或者在列表里混用了可点击按钮——这些边界情况没做隔离,排序就会错乱或卡死。
dragstartdragstartdropondragstarttouchstarttouchmovetouchendtouchstartevent.preventDefault()dataset.sortIndexrequestAnimationFrametransform: translate3d(x, y, 0)let startY = 0;
let startTop = 0;
let isDragging = false;
item.addEventListener('touchstart', e => {
const touch = e.touches[0];
startY = touch.clientY;
startTop = parseFloat(getComputedStyle(item).top) || 0;
isDragging = true;
item.style.transform = translate3d(0, 0, 0);
item.style.transition = 'none';
e.preventDefault();
});
document.addEventListener('touchmove', e => {
if (!isDragging) return;
const touch = e.touches[0];
const dy = touch.clientY - startY;
item.style.transform = translate3d(0, ${startTop + dy}px, 0);
e.preventDefault();
});
document.addEventListener('touchend', () => {
if (!isDragging) return;
// 这里计算最终插入位置,更新数据数组,重渲染列表
isDragging = false;
item.style.transform = '';
item.style.transition = '';
});
appendChild()insertBefore()itemskeydata-key="item-123"valueselectionStartoverflow: scrolltouch-action: nonetouchstartbodytouch-action: nonetouchendtouch-action: autopassive: falsepreventDefault()