
在多指触控(如双指缩放)场景中,当用户抬起某一根手指时,可通过 TouchEvent.changedTouches 精确获取已结束的触摸点,并结合 Touch.identifier 唯一标识符匹配并移除对应缓存数据,从而可靠追踪剩余手指状态。
在多指触控(如双指缩放)场景中,当用户抬起某一根手指时,可通过 `touchevent.changedtouches` 精确获取已结束的触摸点,并结合 `touch.identifier` 唯一标识符匹配并移除对应缓存数据,从而可靠追踪剩余手指状态。
在实现复杂手势(例如双指缩放、三指滑动等)时,一个关键挑战是:当多个手指同时触屏后,部分手指提前抬起(touchend 触发),而其余手指仍在移动(touchmove 继续),此时需准确识别哪几个手指已离开,以便同步更新本地缓存的触摸状态(如 cachedStartTouches 和 cachedMoveTouches)。直接依赖 ev.touches.length 的变化无法定位具体索引——因为 touches 列表本身已剔除了抬起的手指,旧缓存却仍保留全部原始 Touch 对象。
✅ 正确方案:利用 changedTouches 与 identifier
每个 Touch 对象都拥有只读属性 identifier,它在整个触摸生命周期内唯一且稳定(即使手指短暂离开又返回,新 Touch 的 identifier 也会不同)。因此,核心逻辑是:
- 在 touchstart / touchmove 中,将 TouchList 转为数组并按 identifier 缓存;
- 在 touchend 中,遍历 ev.changedTouches 获取所有本次结束的触摸点;
- 通过 identifier 匹配,从缓存数组中精准删除对应项。
以下是可直接落地的 TypeScript 实现:
// 辅助函数:将 TouchList 转为数组(避免 item() 调用开销)
const touchListToArray = (touches: TouchList): Touch[] => {
return Array.from(touches); // 更简洁,兼容现代环境
};
let cachedStartTouches: Touch[] = [];
let cachedMoveTouches: Touch[] = [];
function onStart(ev: TouchEvent) {
cachedStartTouches = touchListToArray(ev.touches);
}
function onMove(ev: TouchEvent) {
cachedMoveTouches = touchListToArray(ev.touches);
}
function onEnd(ev: TouchEvent) {
const endedTouches = touchListToArray(ev.changedTouches); // ✅ 关键:本次抬起的所有 Touch
// 从缓存中移除所有已结束的触摸(支持多指同时抬起)
const endedIds = new Set(endedTouches.map(t => t.identifier));
cachedStartTouches = cachedStartTouches.filter(t => !endedIds.has(t.identifier));
cachedMoveTouches = cachedMoveTouches.filter(t => !endedIds.has(t.identifier));
}⚠️ 注意事项
- 不要依赖索引位置:touches 列表顺序不保证与手指物理顺序或起始顺序一致,且在部分手指抬起后会动态重排,故绝对不可用 indexOf() 或简单比对 clientX/clientY 来推断“哪个手指被抬起”。
- identifier 是唯一可靠标识:它是浏览器分配的数字 ID,同一触摸生命周期内恒定,且不同手指间绝不重复。
- changedTouches 的语义明确:在 touchend 中,它只包含本次事件中结束的触摸点;在 touchstart 中则为新接触点;在 touchmove 中为位置发生变化的点。
- 性能提示:若需高频处理(如每帧计算缩放比例),建议使用 Map<number, Touch> 以 identifier 为 key 实现 O(1) 删除,而非 filter() 数组操作。
通过该模式,无论初始是 2 指、3 指还是 N 指协同操作,都能健壮、无歧义地响应任意子集手指的抬起行为,为手势识别提供坚实的数据基础。

















