1. 移动端触控事件中的两个关键对象
在移动端Web开发中,触摸事件处理是每个前端工程师必须掌握的核心技能。当用户的手指在屏幕上滑动时,浏览器会触发一系列触摸事件,而changedTouches和touches这两个对象常常让开发者感到困惑。它们都存储着触摸点的信息,但在不同场景下表现却大不相同。
我清楚地记得第一次处理多点触控时的混乱场景——当两个手指同时在屏幕上滑动时,控制台打印出的触摸点数据让我完全摸不着头脑。经过多次调试和查阅文档,才真正理解了这两个对象的本质区别。本文将结合具体案例,深入剖析changedTouches和touches的异同点。
2. 基础概念解析:触摸事件的生命周期
2.1 触摸事件的三种基本类型
移动端浏览器主要支持三种触摸事件:
- touchstart:手指触摸屏幕时触发
- touchmove:手指在屏幕上滑动时触发
- touchend:手指离开屏幕时触发
每个事件对象都包含三个关键的TouchList集合:
- touches:当前屏幕上所有触摸点的集合
- targetTouches:当前元素上所有触摸点的集合
- changedTouches:引发当前事件的触摸点集合
2.2 touches对象的特性分析
touches对象包含了屏幕上所有处于活动状态的触摸点信息。这个集合的特点是:
- 在touchstart事件中,包含刚接触屏幕的触摸点
- 在touchmove事件中,包含所有正在移动的触摸点
- 在touchend事件中,touches集合将不再包含已离开的触摸点
element.addEventListener('touchmove', function(e) { console.log('当前屏幕触摸点数量:', e.touches.length); // 遍历所有触摸点 for (let i = 0; i < e.touches.length; i++) { console.log(`触摸点${i}坐标:`, e.touches[i].pageX, e.touches[i].pageY); } });2.3 changedTouches的特殊行为
changedTouches对象只包含引发当前事件的触摸点,它的行为更加特殊:
- 在touchstart中,包含新接触的触摸点
- 在touchmove中,包含位置发生变化的触摸点
- 在touchend中,包含刚刚离开的触摸点
这个特性使得changedTouches在处理特定场景时非常有用,特别是在touchend事件中,它是唯一能获取到已离开触摸点信息的途径。
3. 关键差异对比与使用场景
3.1 数据完整性的差异
在touchend事件中,两者的表现差异最为明显:
- touches集合中已经移除了离开的触摸点
- changedTouches仍然包含刚刚离开的触摸点信息
这个特性对于实现滑动结束时的操作非常关键。例如,在实现一个滑动删除功能时,我们需要在手指离开时获取最后的触摸点位置:
element.addEventListener('touchend', function(e) { // 错误做法:touches在touchend事件中可能为空 // let lastTouch = e.touches[0]; // 正确做法:使用changedTouches获取离开的触摸点 let lastTouch = e.changedTouches[0]; console.log('离开时的位置:', lastTouch.pageX, lastTouch.pageY); });3.2 多点触控处理的差异
当处理多点触控时,两者的区别更加明显。假设两个手指同时触摸屏幕:
- touches会包含两个触摸点
- changedTouches在touchstart事件中也会包含两个触摸点
但当其中一个手指移动时:
- touches仍然包含两个触摸点
- changedTouches只包含移动的那个触摸点
这个特性对于实现精细的多点触控交互非常有用。例如,在实现图片缩放功能时,我们可以这样处理:
let initialDistance; element.addEventListener('touchstart', function(e) { if (e.touches.length === 2) { initialDistance = getDistance(e.touches[0], e.touches[1]); } }); element.addEventListener('touchmove', function(e) { if (e.touches.length === 2 && e.changedTouches.length === 1) { // 只有一个手指在移动的情况 let currentDistance = getDistance(e.touches[0], e.touches[1]); let scale = currentDistance / initialDistance; element.style.transform = `scale(${scale})`; } }); function getDistance(touch1, touch2) { let dx = touch1.pageX - touch2.pageX; let dy = touch1.pageY - touch2.pageY; return Math.sqrt(dx * dx + dy * dy); }3.3 性能考量的差异
从性能角度考虑,changedTouches通常比touches更高效:
- changedTouches只包含变化的触摸点,数据量更小
- 在只需要处理变化触摸点的场景下,使用changedTouches可以减少不必要的遍历
特别是在高频触发的touchmove事件中,这个差异会更加明显。例如,在处理绘图应用时:
// 更高效的做法 canvas.addEventListener('touchmove', function(e) { let ctx = canvas.getContext('2d'); e.changedTouches.forEach(touch => { ctx.lineTo(touch.pageX, touch.pageY); ctx.stroke(); }); }); // 相对低效的做法 canvas.addEventListener('touchmove', function(e) { let ctx = canvas.getContext('2d'); e.touches.forEach(touch => { // 遍历所有触摸点,包括未移动的 ctx.lineTo(touch.pageX, touch.pageY); ctx.stroke(); }); });4. 实战中的常见问题与解决方案
4.1 触摸点丢失问题
很多开发者会遇到touchend事件中无法获取触摸点的问题,这是因为:
- 在touchend事件触发时,离开的触摸点已经从touches集合中移除
- 必须使用changedTouches才能获取到离开的触摸点信息
解决方案示例:
// 错误示例:无法获取离开的触摸点 element.addEventListener('touchend', function(e) { console.log(e.touches.length); // 输出0 }); // 正确示例:使用changedTouches element.addEventListener('touchend', function(e) { let touch = e.changedTouches[0]; console.log('离开的触摸点:', touch.identifier, touch.pageX, touch.pageY); });4.2 触摸点追踪问题
在需要追踪特定触摸点的场景下(如实现拖拽功能),identifier属性就变得非常重要。每个触摸点都有一个唯一的identifier,在整个触摸过程中保持不变。
实现方案:
let activeTouchId = null; element.addEventListener('touchstart', function(e) { if (activeTouchId === null) { activeTouchId = e.changedTouches[0].identifier; // 开始拖拽逻辑 } }); element.addEventListener('touchmove', function(e) { // 在touches集合中查找特定identifier的触摸点 for (let i = 0; i < e.touches.length; i++) { if (e.touches[i].identifier === activeTouchId) { // 更新位置 updatePosition(e.touches[i].pageX, e.touches[i].pageY); break; } } }); element.addEventListener('touchend', function(e) { // 检查离开的触摸点是否是正在追踪的那个 for (let i = 0; i < e.changedTouches.length; i++) { if (e.changedTouches[i].identifier === activeTouchId) { activeTouchId = null; // 结束拖拽逻辑 break; } } });4.3 跨浏览器兼容性问题
虽然大多数现代浏览器都支持触摸事件,但在细节实现上仍有一些差异需要注意:
- 某些旧版本浏览器可能不会为每个触摸点生成唯一的identifier
- 部分浏览器在touchmove事件中changedTouches可能包含所有触摸点,而不仅仅是变化的
- 触摸点坐标系统在不同浏览器中可能有细微差别
兼容性处理建议:
element.addEventListener('touchmove', function(e) { // 使用特性检测确保identifier存在 let useIdentifier = ('identifier' in e.changedTouches[0]); // 保守处理:假设changedTouches可能包含所有触摸点 let changedPoints = e.changedTouches; if (changedPoints.length === e.touches.length) { // 可能需要额外逻辑判断哪些点真正发生了变化 } // 处理坐标系统差异 let x = e.changedTouches[0].pageX - element.offsetLeft; let y = e.changedTouches[0].pageY - element.offsetTop; });5. 高级应用场景与优化技巧
5.1 手势识别实现
利用changedTouches和touches的差异,我们可以实现更复杂的手势识别。例如,实现一个滑动方向判断:
let startX, startY; element.addEventListener('touchstart', function(e) { startX = e.changedTouches[0].pageX; startY = e.changedTouches[0].pageY; }); element.addEventListener('touchend', function(e) { let endX = e.changedTouches[0].pageX; let endY = e.changedTouches[0].pageY; let dx = endX - startX; let dy = endY - startY; if (Math.abs(dx) > Math.abs(dy)) { if (dx > 0) { console.log('向右滑动'); } else { console.log('向左滑动'); } } else { if (dy > 0) { console.log('向下滑动'); } else { console.log('向上滑动'); } } });5.2 性能优化实践
对于高性能要求的应用(如游戏或绘图应用),触摸事件处理需要特别注意性能:
- 节流处理:对于高频的touchmove事件,可以使用requestAnimationFrame进行节流
let ticking = false; element.addEventListener('touchmove', function(e) { if (!ticking) { requestAnimationFrame(function() { processTouchMove(e.changedTouches); ticking = false; }); ticking = true; } });- 减少DOM操作:缓存DOM查询结果,避免在每次触摸事件中都进行查询
let ctx = canvas.getContext('2d'); let lastPoint = null; canvas.addEventListener('touchmove', function(e) { let touch = e.changedTouches[0]; if (lastPoint) { ctx.beginPath(); ctx.moveTo(lastPoint.x, lastPoint.y); ctx.lineTo(touch.pageX, touch.pageY); ctx.stroke(); } lastPoint = {x: touch.pageX, y: touch.pageY}; });5.3 与Pointer Events的协同
现代浏览器开始支持Pointer Events API,它统一了鼠标、触摸和触控笔事件。在实际项目中,我们可能需要同时处理Touch Events和Pointer Events:
// 检测浏览器支持情况 if (window.PointerEvent) { // 使用Pointer Events element.addEventListener('pointerdown', handlePointerStart); element.addEventListener('pointermove', handlePointerMove); element.addEventListener('pointerup', handlePointerEnd); } else { // 回退到Touch Events element.addEventListener('touchstart', handleTouchStart); element.addEventListener('touchmove', handleTouchMove); element.addEventListener('touchend', handleTouchEnd); // 可选:添加鼠标事件支持 element.addEventListener('mousedown', handleMouseStart); element.addEventListener('mousemove', handleMouseMove); element.addEventListener('mouseup', handleMouseEnd); } // 统一的事件处理函数 function handleStartEvent(e) { let point = e.changedTouches ? e.changedTouches[0] : e; console.log('接触点:', point.pageX, point.pageY); }在实际开发中,理解changedTouches和touches的区别可以避免很多难以调试的问题。特别是在处理复杂的触摸交互时,选择合适的触摸点集合往往能达到事半功倍的效果。经过多个移动端项目的实践,我发现changedTouches在大多数情况下都是更安全可靠的选择,特别是在touchend事件处理中。而touches则更适合需要了解全局触摸状态的场景,如多点触控的实现。