jQuery中使用touchmove事件实现移动端滑动效果的实用技巧与常见问题解析

在移动Web开发中,实现流畅的滑动效果是提升用户体验的关键。jQuery作为经典的DOM操作库,虽然在现代开发中逐渐被原生JavaScript或框架取代,但在许多遗留项目或快速原型开发中仍然广泛使用。本文将深入探讨如何利用jQuery的
touchmove
事件实现移动端滑动效果,涵盖实用技巧、代码示例以及常见问题的解决方案。
一、touchmove事件基础
1.1 什么是touchmove事件?
touchmove
是移动端触摸事件系列中的一员,与
touchstart
、
touchend
共同构成完整的触摸交互流程:
touchstart
:手指接触屏幕时触发
touchmove
:手指在屏幕上滑动时持续触发
touchend
:手指离开屏幕时触发
1.2 与鼠标事件的区别
移动端触摸事件与桌面鼠标事件有本质区别:
// 鼠标事件(桌面端)
$(element).on(‘mousedown’, handler);
$(element).on(‘mousemove’, handler);
$(element).on(‘mouseup’, handler);

// 触摸事件(移动端)
$(element).on(‘touchstart’, handler);
$(element).on(‘touchmove’, handler);
$(element).on(‘touchend’, handler);
1.3 触摸事件对象属性
触摸事件对象包含重要的坐标信息:
$(element).on(‘touchmove’, function(e) {
// 获取触摸点坐标
const touch = e.originalEvent.touches[0] || e.originalEvent.changedTouches[0];
const x = touch.clientX; // 相对于视口的X坐标
const y = touch.clientY; // 相对于视口的Y坐标
const pageX = touch.pageX; // 相对于页面的X坐标
const pageY = touch.pageY; // 相对于页面的Y坐标
});
二、基础滑动效果实现
2.1 简单的水平滑动
实现一个可左右滑动的图片列表:

Slide 1
Slide 2
Slide 3

$(document).ready(function() {
const $slider = $(‘#slider’);
const $content = $(‘#slider-content’);
let startX = 0;
let currentTranslate = 0;
let isDragging = false;

// 触摸开始
$slider.on(‘touchstart’, function(e) {
const touch = e.originalEvent.touches[0];
startX = touch.clientX;
isDragging = true;
$content.css(‘transition’, ‘none’); // 移除过渡效果,使拖动更流畅
});

// 触摸移动
$slider.on(‘touchmove’, function(e) {
if (!isDragging) return;

const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX – startX;
const newTranslate = currentTranslate + deltaX;

// 限制滑动范围(防止滑出边界)
const maxTranslate = 0;
const minTranslate = -($content.children().length – 1) * $slider.width();

if (newTranslate > maxTranslate) {
$content.css(‘transform’, `translateX(${maxTranslate}px)`);
} else if (newTranslate < minTranslate) { $content.css('transform', `translateX(${minTranslate}px)`); } else { $content.css('transform', `translateX(${newTranslate}px)`); } e.preventDefault(); // 阻止默认滚动行为 }); // 触摸结束 $slider.on('touchend', function(e) { if (!isDragging) return; isDragging = false; $content.css('transition', 'transform 0.3s ease'); // 计算最终位置(吸附到最近的幻灯片) const currentTransform = $content.css('transform'); const matrix = currentTransform.match(/matrix.*\((.+)\)/); let currentX = 0; if (matrix) { const values = matrix[1].split(', '); currentX = parseFloat(values[4]); } // 计算应该吸附到哪个幻灯片 const slideWidth = $slider.width(); const slideIndex = Math.round(-currentX / slideWidth); const targetTranslate = -slideIndex * slideWidth; $content.css('transform', `translateX(${targetTranslate}px)`); currentTranslate = targetTranslate; }); }); 2.2 垂直滑动(下拉刷新效果) $(document).ready(function() { const $container = $('#pull-to-refresh'); const $refreshIndicator = $('#refresh-indicator'); let startY = 0; let currentY = 0; let isPulling = false; const threshold = 80; // 触发刷新的阈值 $container.on('touchstart', function(e) { const touch = e.originalEvent.touches[0]; startY = touch.clientY; isPulling = true; $container.css('transition', 'none'); }); $container.on('touchmove', function(e) { if (!isPulling) return; const touch = e.originalEvent.touches[0]; const deltaY = touch.clientY - startY; // 只处理向下拉的情况 if (deltaY > 0) {
currentY = deltaY;
$container.css(‘transform’, `translateY(${deltaY}px)`);

// 更新刷新指示器
if (deltaY > threshold) {
$refreshIndicator.text(‘松开刷新…’);
} else {
$refreshIndicator.text(‘下拉刷新…’);
}

e.preventDefault();
}
});

$container.on(‘touchend’, function(e) {
if (!isPulling) return;

isPulling = false;
$container.css(‘transition’, ‘transform 0.3s ease’);

if (currentY > threshold) {
// 触发刷新
$container.css(‘transform’, ‘translateY(80px)’);
$refreshIndicator.text(‘正在刷新…’);

// 模拟异步刷新操作
setTimeout(function() {
$container.css(‘transform’, ‘translateY(0)’);
$refreshIndicator.text(‘刷新完成!’);

setTimeout(function() {
$refreshIndicator.text(‘下拉刷新…’);
}, 1000);
}, 2000);
} else {
// 回弹到原位置
$container.css(‘transform’, ‘translateY(0)’);
}

currentY = 0;
});
});
三、实用技巧
3.1 性能优化:使用requestAnimationFrame
在
touchmove
事件中频繁操作DOM会导致性能问题,使用
requestAnimationFrame
可以优化:
let rafId = null;
let pendingTransform = null;

$slider.on(‘touchmove’, function(e) {
if (!isDragging) return;

const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX – startX;
pendingTransform = currentTranslate + deltaX;

// 取消之前的动画帧
if (rafId) {
cancelAnimationFrame(rafId);
}

// 使用requestAnimationFrame批量更新
rafId = requestAnimationFrame(function() {
$content.css(‘transform’, `translateX(${pendingTransform}px)`);
rafId = null;
});

e.preventDefault();
});
3.2 多点触控处理
现代移动设备支持多点触控,需要正确处理:
let activeTouches = [];

$slider.on(‘touchstart’, function(e) {
// 获取所有触摸点
activeTouches = Array.from(e.originalEvent.touches);
startX = activeTouches[0].clientX;
});

$slider.on(‘touchmove’, function(e) {
// 更新触摸点信息
activeTouches = Array.from(e.originalEvent.touches);

// 如果有多个触摸点,可以计算缩放或旋转
if (activeTouches.length >= 2) {
const touch1 = activeTouches[0];
const touch2 = activeTouches[1];

// 计算两点距离(用于缩放)
const distance = Math.sqrt(
Math.pow(touch2.clientX – touch1.clientX, 2) +
Math.pow(touch2.clientY – touch1.clientY, 2)
);

// 计算角度(用于旋转)
const angle = Math.atan2(
touch2.clientY – touch1.clientY,
touch2.clientX – touch1.clientX
) * 180 / Math.PI;

// 应用缩放和旋转
$content.css(‘transform’, `scale(${distance / 100}) rotate(${angle}deg)`);
}
});
3.3 惯性滑动(动量滚动)
添加惯性效果使滑动更自然:
let velocity = 0;
let lastTime = 0;
let lastX = 0;

$slider.on(‘touchmove’, function(e) {
const now = Date.now();
const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX – lastX;
const deltaTime = now – lastTime;

// 计算速度(像素/毫秒)
velocity = deltaX / deltaTime;

lastX = touch.clientX;
lastTime = now;
});

$slider.on(‘touchend’, function(e) {
// 应用惯性滑动
const friction = 0.95; // 摩擦系数
const minVelocity = 0.1; // 最小速度阈值

function applyInertia() {
if (Math.abs(velocity) < minVelocity) { // 速度太小,停止惯性滑动 return; } // 应用摩擦力 velocity *= friction; // 计算位移 const displacement = velocity * 16; // 假设60fps,每帧约16ms // 更新位置 currentTranslate += displacement; // 边界检查 const maxTranslate = 0; const minTranslate = -($content.children().length - 1) * $slider.width(); if (currentTranslate > maxTranslate) {
currentTranslate = maxTranslate;
velocity = 0;
} else if (currentTranslate < minTranslate) { currentTranslate = minTranslate; velocity = 0; } $content.css('transform', `translateX(${currentTranslate}px)`); // 继续下一帧 requestAnimationFrame(applyInertia); } // 开始惯性滑动 requestAnimationFrame(applyInertia); }); 3.4 防抖与节流 在 touchmove 事件中,可以使用防抖或节流来减少事件处理频率: // 节流函数 function throttle(func, limit) { let inThrottle; return function() { const args = arguments; const context = this; if (!inThrottle) { func.apply(context, args); inThrottle = true; setTimeout(() => inThrottle = false, limit);
}
};
}

// 使用节流处理touchmove
const throttledTouchMove = throttle(function(e) {
// 处理滑动逻辑
const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX – startX;
$content.css(‘transform’, `translateX(${currentTranslate + deltaX}px)`);
}, 16); // 约60fps

$slider.on(‘touchmove’, throttledTouchMove);
四、常见问题与解决方案
4.1 问题:页面滚动与滑动冲突
现象
:在滑动组件时,页面也会跟着滚动。
解决方案
:
// 方案1:在touchmove中阻止默认行为
$slider.on(‘touchmove’, function(e) {
e.preventDefault(); // 阻止页面滚动
});

// 方案2:使用CSS属性控制
#slider {
touch-action: pan-y; /* 允许垂直滚动,禁止水平滚动 */
/* 或者 */
touch-action: none; /* 完全禁止所有触摸行为 */
}

// 方案3:判断滑动方向,只在水平滑动时阻止
let isHorizontal = false;
$slider.on(‘touchstart’, function(e) {
const touch = e.originalEvent.touches[0];
startX = touch.clientX;
startY = touch.clientY;
isHorizontal = false;
});

$slider.on(‘touchmove’, function(e) {
const touch = e.originalEvent.touches[0];
const deltaX = touch.clientX – startX;
const deltaY = touch.clientY – startY;

// 如果水平位移大于垂直位移,认为是水平滑动
if (Math.abs(deltaX) > Math.abs(deltaY) && !isHorizontal) {
isHorizontal = true;
}

if (isHorizontal) {
e.preventDefault(); // 只阻止水平滑动时的页面滚动
}
});
4.2 问题:iOS设备上的300ms延迟
现象
:在iOS设备上,点击事件有300ms延迟,影响滑动体验。
解决方案
:
// 方案1:使用touch事件代替click事件
// 不要使用click事件,使用touchend
$slider.on(‘touchend’, function(e) {
// 处理点击逻辑
});

// 方案2:设置viewport meta标签
//

// 方案3:使用FastClick库(如果需要支持旧版iOS)
// 引入FastClick后,它会自动处理300ms延迟问题
4.3 问题:Android设备上的性能问题
现象
:在低端Android设备上,滑动卡顿。
解决方案
:
// 方案1:减少DOM操作
// 避免在touchmove中频繁读取或修改DOM属性
// 使用transform代替left/top属性(GPU加速)
$slider.on(‘touchmove’, function(e) {
// 好的做法
$content.css(‘transform’, `translateX(${deltaX}px)`);

// 避免的做法
// $content.css(‘left’, `${deltaX}px`);
});

// 方案2:使用will-change属性
#slider-content {
will-change: transform; /* 提示浏览器优化 */
}

// 方案3:减少事件监听器
// 如果可能,使用事件委托
$(document).on(‘touchmove’, ‘.slider’, function(e) {
// 处理滑动逻辑
});
4.4 问题:触摸点丢失
现象
:在滑动过程中,触摸点突然丢失,导致滑动中断。
解决方案
:
// 方案1:使用changedTouches
$slider.on(‘touchend’, function(e) {
// 使用changedTouches而不是touches
const touch = e.originalEvent.changedTouches[0];
// 处理逻辑
});

// 方案2:添加触摸点追踪
let touchPoints = new Map();

$slider.on(‘touchstart’, function(e) {
Array.from(e.originalEvent.touches).forEach(touch => {
touchPoints.set(touch.identifier, {
x: touch.clientX,
y: touch.clientY
});
});
});

$slider.on(‘touchmove’, function(e) {
// 更新触摸点位置
Array.from(e.originalEvent.touches).forEach(touch => {
if (touchPoints.has(touch.identifier)) {
touchPoints.get(touch.identifier).x = touch.clientX;
touchPoints.get(touch.identifier).y = touch.clientY;
}
});
});

$slider.on(‘touchend’, function(e) {
// 清除离开的触摸点
Array.from(e.originalEvent.changedTouches).forEach(touch => {
touchPoints.delete(touch.identifier);
});
});
4.5 问题:横竖屏切换导致的布局问题
现象
:横竖屏切换后,滑动组件宽度变化,导致滑动位置错乱。
解决方案
:
// 监听窗口大小变化
$(window).on(‘resize orientationchange’, function() {
// 重新计算滑动位置
const slideWidth = $slider.width();
const currentSlideIndex = Math.round(-currentTranslate / slideWidth);

// 重新定位到当前幻灯片
currentTranslate = -currentSlideIndex * slideWidth;
$content.css(‘transform’, `translateX(${currentTranslate}px)`);

// 更新触摸起始位置
startX = 0;
});
五、高级应用:实现轮播图组件
5.1 完整的轮播图实现
(function($) {
// 轮播图构造函数
function TouchSlider(element, options) {
this.$element = $(element);
this.options = $.extend({
autoplay: true,
interval: 3000,
loop: true,
speed: 300,
threshold: 50
}, options);

this.init();
}

TouchSlider.prototype = {
init: function() {
this.$content = this.$element.find(‘.slider-content’);
this.$slides = this.$content.children();
this.slideCount = this.$slides.length;
this.currentIndex = 0;
this.isDragging = false;
this.startX = 0;
this.currentTranslate = 0;

// 克隆首尾幻灯片实现循环
if (this.options.loop) {
this.$content.prepend(this.$slides.last().clone());
this.$content.append(this.$slides.first().clone());
this.slideCount += 2;
this.currentIndex = 1;
}

this.bindEvents();
this.startAutoplay();
},

bindEvents: function() {
const self = this;

// 触摸事件
this.$element.on(‘touchstart’, function(e) {
self.onTouchStart(e);
});

this.$element.on(‘touchmove’, function(e) {
self.onTouchMove(e);
});

this.$element.on(‘touchend’, function(e) {
self.onTouchEnd(e);
});

// 窗口大小变化
$(window).on(‘resize orientationchange’, function() {
self.onResize();
});

// 鼠标事件(桌面端兼容)
this.$element.on(‘mousedown’, function(e) {
self.onTouchStart(e);
});

this.$element.on(‘mousemove’, function(e) {
self.onTouchMove(e);
});

this.$element.on(‘mouseup’, function(e) {
self.onTouchEnd(e);
});

this.$element.on(‘mouseleave’, function(e) {
self.onTouchEnd(e);
});
},

onTouchStart: function(e) {
// 停止自动播放
this.stopAutoplay();

const touch = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
this.startX = touch.clientX;
this.isDragging = true;
this.$content.css(‘transition’, ‘none’);

// 记录当前translate值
const currentTransform = this.$content.css(‘transform’);
const matrix = currentTransform.match(/matrix.*\((.+)\)/);
if (matrix) {
const values = matrix[1].split(‘, ‘);
this.currentTranslate = parseFloat(values[4]);
}
},

onTouchMove: function(e) {
if (!this.isDragging) return;

const touch = e.originalEvent.touches ? e.originalEvent.touches[0] : e;
const deltaX = touch.clientX – this.startX;
const newTranslate = this.currentTranslate + deltaX;

// 边界检查
const maxTranslate = 0;
const minTranslate = -(this.slideCount – 1) * this.$element.width();

if (newTranslate > maxTranslate) {
this.$content.css(‘transform’, `translateX(${maxTranslate}px)`);
} else if (newTranslate < minTranslate) { this.$content.css('transform', `translateX(${minTranslate}px)`); } else { this.$content.css('transform', `translateX(${newTranslate}px)`); } e.preventDefault(); }, onTouchEnd: function(e) { if (!this.isDragging) return; this.isDragging = false; this.$content.css('transition', `transform ${this.options.speed}ms ease`); // 获取最终位置 const currentTransform = this.$content.css('transform'); const matrix = currentTransform.match(/matrix.*\((.+)\)/); let currentX = 0; if (matrix) { const values = matrix[1].split(', '); currentX = parseFloat(values[4]); } // 计算滑动距离 const slideWidth = this.$element.width(); const deltaX = currentX - this.currentTranslate; // 判断是否达到阈值 if (Math.abs(deltaX) > this.options.threshold) {
// 滑动到下一张或上一张
if (deltaX < 0) { this.currentIndex++; } else { this.currentIndex--; } } // 边界处理(循环模式) if (this.options.loop) { if (this.currentIndex === 0) { this.currentIndex = this.slideCount - 2; this.$content.css('transition', 'none'); this.$content.css('transform', `translateX(${-this.currentIndex * slideWidth}px)`); setTimeout(() => {
this.$content.css(‘transition’, `transform ${this.options.speed}ms ease`);
}, 10);
} else if (this.currentIndex === this.slideCount – 1) {
this.currentIndex = 1;
this.$content.css(‘transition’, ‘none’);
this.$content.css(‘transform’, `translateX(${-this.currentIndex * slideWidth}px)`);

setTimeout(() => {
this.$content.css(‘transition’, `transform ${this.options.speed}ms ease`);
}, 10);
}
}

// 应用最终位置
const targetTranslate = -this.currentIndex * slideWidth;
this.$content.css(‘transform’, `translateX(${targetTranslate}px)`);
this.currentTranslate = targetTranslate;

// 重新开始自动播放
this.startAutoplay();

// 触发事件
this.$element.trigger(‘slider:slide’, [this.currentIndex]);
},

onResize: function() {
const slideWidth = this.$element.width();
const targetTranslate = -this.currentIndex * slideWidth;
this.$content.css(‘transform’, `translateX(${targetTranslate}px)`);
this.currentTranslate = targetTranslate;
},

startAutoplay: function() {
if (!this.options.autoplay) return;

this.stopAutoplay();
this.autoplayTimer = setInterval(() => {
this.next();
}, this.options.interval);
},

stopAutoplay: function() {
if (this.autoplayTimer) {
clearInterval(this.autoplayTimer);
this.autoplayTimer = null;
}
},

next: function() {
if (this.isDragging) return;

this.currentIndex++;

if (this.options.loop) {
if (this.currentIndex === this.slideCount – 1) {
this.currentIndex = 1;
this.$content.css(‘transition’, ‘none’);
this.$content.css(‘transform’, `translateX(${-this.currentIndex * this.$element.width()}px)`);

setTimeout(() => {
this.$content.css(‘transition’, `transform ${this.options.speed}ms ease`);
}, 10);
}
}

const targetTranslate = -this.currentIndex * this.$element.width();
this.$content.css(‘transform’, `translateX(${targetTranslate}px)`);
this.currentTranslate = targetTranslate;
},

prev: function() {
if (this.isDragging) return;

this.currentIndex–;

if (this.options.loop) {
if (this.currentIndex === 0) {
this.currentIndex = this.slideCount – 2;
this.$content.css(‘transition’, ‘none’);
this.$content.css(‘transform’, `translateX(${-this.currentIndex * this.$element.width()}px)`);

setTimeout(() => {
this.$content.css(‘transition’, `transform ${this.options.speed}ms ease`);
}, 10);
}
}

const targetTranslate = -this.currentIndex * this.$element.width();
this.$content.css(‘transform’, `translateX(${targetTranslate}px)`);
this.currentTranslate = targetTranslate;
}
};

// jQuery插件封装
$.fn.touchSlider = function(options) {
return this.each(function() {
if (!$.data(this, ‘touchSlider’)) {
$.data(this, ‘touchSlider’, new TouchSlider(this, options));
}
});
};
})(jQuery);

// 使用示例
$(document).ready(function() {
$(‘#mySlider’).touchSlider({
autoplay: true,
interval: 4000,
loop: true,
speed: 400,
threshold: 30
});

// 手动控制
$(‘#mySlider’).data(‘touchSlider’).next();
$(‘#mySlider’).data(‘touchSlider’).prev();
});
5.2 HTML结构示例

Slide 1
Slide 2
Slide 3
Slide 4




六、最佳实践总结
6.1 代码组织建议
模块化
:将滑动逻辑封装成独立的模块或插件
配置化
:提供可配置的选项,如速度、阈值、自动播放等
事件驱动
:使用自定义事件,便于与其他组件交互
6.2 性能优化清单
✅ 使用
transform
代替
left/top
(GPU加速)
✅ 使用
will-change
提示浏览器优化
✅ 使用
requestAnimationFrame
批量更新
✅ 避免在
touchmove
中读取布局属性(如
offsetWidth
)
✅ 使用事件委托减少事件监听器数量
6.3 兼容性处理
桌面端兼容
:同时监听鼠标事件
旧版浏览器
:检测
touch
事件支持,提供降级方案
触摸点追踪
:正确处理多点触控和触摸点丢失
6.4 测试建议
真机测试
:在不同设备和浏览器上测试
性能测试
:使用Chrome DevTools的Performance面板
边界测试
:测试快速滑动、长按、多点触控等场景
七、替代方案与现代选择
虽然jQuery的touchmove实现仍然有效,但在现代开发中,有更优的选择:
7.1 原生JavaScript实现
// 使用原生API,减少依赖
element.addEventListener(‘touchstart’, handler, { passive: false });
7.2 现代库推荐
Swiper.js
:功能强大的轮播图库
Hammer.js
:手势识别库
AlloyTouch
:轻量级触摸库
7.3 框架集成
Vue.js
:使用
vue-touch
或自定义指令
React
:使用
react-swipeable
等组件
八、总结
jQuery的touchmove事件在移动端滑动效果实现中仍然具有实用价值,特别是在维护旧项目或快速原型开发时。通过合理的性能优化、边界处理和兼容性考虑,可以创建出流畅的滑动体验。
关键要点:
正确处理触摸事件序列
:
touchstart
→
touchmove
→
touchend
性能优化
:使用
transform
、
requestAnimationFrame
、
will-change
边界处理
:防止滑出可视区域,处理循环模式
兼容性
:同时支持触摸和鼠标事件
用户体验
:添加惯性滑动、防抖节流、自动播放等增强功能
随着Web技术的发展,虽然原生API和现代库提供了更好的性能和更简洁的API,但理解jQuery实现touchmove的原理和技巧,对于深入理解移动端交互机制仍然非常有价值。