移动Web端事件
z在早期的时候,是没有移动端事件,大部分鼠标事件在移动端一样有用,后面苹果推出了touch 触摸事件,后续浏览器跟进,微软推出pointer指针事件(统一鼠标、触摸、笔),那在PC和移动端表现有什么区别,为什么要学习,举一个轮播图例子,正常在pc端通过点击左右按钮切换轮播,而在移动端,可以通过触摸滑动图片实现轮播的切换
Touch 触摸事件
Touch事件基础
touch事件类型有以下4种:
| 事件 | 触发时机 |
|---|---|
touchstart | 手指接触屏幕 |
touchmove | 手指在屏幕上移动 |
touchend | 手指离开屏幕 |
touchcancel | 触摸操作被浏览器或系统中断 |
示例:
// touch事件类型// touchstart touchmove touchend touchcancelconst $box = document.getElementById('box');//触摸开始$box.addEventListener( 'touchstart', function (e) { console.log('手指接触屏幕', e); }, false,);// 移动$box.addEventListener( 'touchmove', function (e) { console.log('手指在屏幕上移动', e); }, false,);// 离开$box.addEventListener( 'touchend', function (e) { console.log('手指离开屏幕', e); }, false,);// 触摸操作被浏览器或系统中断$box.addEventListener( 'touchcancel', function (e) { console.log('触摸操作被浏览器或系统中断', e); }, false,);注意:即使触摸点移出目标元素,touchmove事件依然会持续触发,前提是手指一直触摸不要离开屏幕的触摸滑动,而mousemove这个鼠标事件不会触发;
Touch事件在pc端不会触发,鼠标事件在PC和移动端都会触发;touch事件的特征检测可以通过 'ontouchstart' in window检测是否支持,返回一个布尔值
Touch事件的event对象
event对象常用属性
| 属性 / 方法 | 作用 |
|---|---|
event.touches | 当前仍接触屏幕的所有触摸点 |
event.targetTouches | 当前仍接触屏幕,且最初在事件目标元素上开始触摸的触摸点 |
event.changedTouches | 本次事件中发生变化的触摸点 |
event.target | 最初触发事件的元素 |
event.currentTarget | 当前绑定事件处理程序的元素 |
event.type | 事件类型,如 touchstart |
event.cancelable | 当前事件是否可以取消默认行为 |
event.preventDefault() | 阻止浏览器的默认行为 |
其中,touches、targetTouches 和 changedTouches 是 Touch 事件最重要的三个属性。
box.addEventListener('touchstart', (event) => { console.log(event.touches); console.log(event.targetTouches); console.log(event.changedTouches);});触摸点的常用属性
上面的三个属性返回的都是 TouchList 对象,可以通过下标获取其中的触摸点。
box.addEventListener('touchstart', (event) => { const touch = event.changedTouches[0];
console.log(touch.clientX); console.log(touch.clientY);});常用的触摸点属性如下:
| 属性 | 作用 |
|---|---|
identifier | 触摸点的唯一标识,用于区分不同触摸点 |
target | 该触摸点最初开始触摸的元素 |
clientX / clientY | 触摸点相对于浏览器视口的坐标 |
pageX / pageY | 触摸点相对于整个文档的坐标 |
screenX / screenY | 触摸点相对于屏幕的坐标 |
radiusX / radiusY | 触摸接触区域的椭圆半径 |
rotationAngle | 触摸接触区域的旋转角度 |
force | 触摸压力值,通常在 0 到 1 之间 |
日常开发中,最常用的是 clientX、clientY,例如获取手指的位置、计算滑动距离。
阻止浏览器的默认行为
移动端触摸操作可能触发浏览器的默认行为,例如页面滚动、缩放等。
可以使用 event.preventDefault() 阻止可取消的默认行为。
box.addEventListener( 'touchmove', (event) => { event.preventDefault(); }, { passive: false },);这里需要注意两点:
preventDefault()是阻止默认行为,不是阻止事件冒泡;阻止冒泡使用stopPropagation()。- 现代浏览器中,部分触摸事件监听器默认可能采用被动监听(
passive)。被动监听器中调用preventDefault()无法阻止默认行为,因此需要时应明确设置{ passive: false }。
实际开发建议: 如果只是要控制页面是否允许滚动或缩放,优先考虑 CSS 的 touch-action;只有确实需要通过 JavaScript 取消默认行为时,再使用 preventDefault()。
单指拖拽
单指拖拽是指用户用一根手指按住元素并移动,从而改变元素的位置。
实现思路:
touchstart:记录手指初始坐标和元素初始位置。touchmove:计算手指移动距离,更新元素位置。touchend:结束拖拽。
示例代码
<div class="box">拖动我</div><style> .box { position: fixed; left: 50px; top: 100px;
width: 120px; height: 120px; background-color: skyblue;
display: flex; justify-content: center; align-items: center;
touch-action: none; user-select: none; }</style><script> const box = document.querySelector('.box');
let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; let dragging = false;
box.addEventListener( 'touchstart', (event) => { // 只允许单指拖拽 if (event.touches.length !== 1) { dragging = false; return; }
const touch = event.changedTouches[0]; const rect = box.getBoundingClientRect();
// 记录手指初始坐标 startX = touch.clientX; startY = touch.clientY;
// 记录元素初始位置 startLeft = rect.left; startTop = rect.top;
dragging = true; }, { passive: true }, );
box.addEventListener( 'touchmove', (event) => { if (!dragging) return;
// 多指触摸时取消拖拽 if (event.touches.length !== 1) { dragging = false; return; }
const touch = event.touches[0];
// 计算手指移动距离 const moveX = touch.clientX - startX; const moveY = touch.clientY - startY;
// 更新元素位置 box.style.left = `${startLeft + moveX}px`; box.style.top = `${startTop + moveY}px`; }, { passive: true }, );
box.addEventListener('touchend', () => { dragging = false; });
box.addEventListener('touchcancel', () => { dragging = false; });</script>核心计算公式
| 变量 | 计算方式 |
|---|---|
| 水平移动距离 | moveX = 当前手指X - 初始手指X |
| 垂直移动距离 | moveY = 当前手指Y - 初始手指Y |
| 元素新位置 | 初始位置 + 移动距离 |
例如,手指初始横坐标为 100px,移动后变成 150px,那么:
- 手指水平移动了
50px。 - 如果元素原本位于
200px,新位置就是250px。
注意事项
touch-action: none用于禁止浏览器在该元素上执行原生平移、缩放手势,避免与自定义拖拽冲突。- 如果改用
event.preventDefault()阻止默认行为,touchmove监听器需要设置{ passive: false },否则可能无法阻止。 touchend和touchcancel都要处理,避免拖拽状态没有正确结束。
记住核心思路:记录初始位置 → 计算手指移动距离 → 更新元素位置。
pointer指针事件
Pointer Events(指针事件) 是统一处理鼠标、触摸屏、触控笔等输入设备的事件机制。与 Mouse Events 和 Touch Events 相比,Pointer Events 最大的优势是:一套事件代码,可以同时处理 PC 端和移动端的交互。
Pointer事件基础
Pointer事件类型
pointerdown | 指针按下时 | 开始拖拽、按压 |
|---|---|---|
pointermove | 指针移动时 | 拖拽、绘图 |
pointerup | 指针释放时 | 结束拖拽 |
pointercancel | 指针操作被浏览器取消时 | 清理拖拽状态 |
pointerover | 指针进入元素区域 | 悬停效果 |
pointerout | 指针离开元素区域 | 取消悬停 |
pointerenter | 指针进入元素或其子元素区域 | 鼠标进入 |
pointerleave | 指针离开元素及其子元素区域 | 鼠标离开 |
gotpointercapture | 元素获得指针捕获时 | 监听捕获状态 |
lostpointercapture | 元素失去指针捕获时 | 监听捕获释放 |
pointerrawupdate | 指针状态发生变化时,提供更直接的更新 | 高精度绘图等 |
其中,最常用的是 pointerdown、pointermove、pointerup、pointercancel
实现一个简单的拖拽操作:
const box = document.querySelector('.box');
let isDragging = false;
box.addEventListener('pointerdown', (e) => { isDragging = true; box.setPointerCapture(e.pointerId);});
box.addEventListener('pointermove', (e) => { if (!isDragging) return;
console.log(e.clientX, e.clientY);});
box.addEventListener('pointerup', () => { isDragging = false;});
box.addEventListener('pointercancel', () => { isDragging = false;});Pointer 事件注意事项
- Pointer 事件不会自动阻止浏览器默认行为
在移动端,浏览器默认会处理页面滚动、缩放等触摸操作。如果使用 Pointer 事件实现拖拽,可能触发 pointercancel,导致拖拽中断。解决方法:使用 CSS 的 touch-action 属性。
.box { touch-action: none;}常见取值:
| 属性值 | 作用 |
|---|---|
auto | 浏览器默认处理触摸操作 |
none | 禁止浏览器处理该区域的触摸平移和缩放手势 |
pan-x | 允许浏览器处理水平滚动 |
pan-y | 允许浏览器处理垂直滚动 |
manipulation | 允许滚动和双指缩放等操作,不允许双击缩放 |
注意:touch-action: none 应只设置在需要自定义触摸交互的区域,避免影响正常页面滚动。
- pointermove 不一定需要按下指针
pointermove 只要指针移动就可能触发。例如,鼠标没有按下,只是在元素内部移动,也会触发 pointermove。因此实现拖拽时,需要判断是否处于拖拽状态。
let isDragging = false;
box.addEventListener('pointerdown', () => { isDragging = true;});
box.addEventListener('pointermove', (e) => { if (!isDragging) return;
console.log(e.clientX, e.clientY);});
box.addEventListener('pointerup', () => { isDragging = false;});
box.addEventListener('pointercancel', () => { isDragging = false;});- 指针离开元素可能导致事件无法继续接收
默认情况下,鼠标移出元素后,元素可能无法继续接收后续的 pointermove 和 pointerup。解决方法:使用指针捕获。
box.addEventListener('pointerdown', (e) => { box.setPointerCapture(e.pointerId);});这样即使指针移出元素,后续事件仍然可以发送到该元素。
相关 API:
| API | 作用 |
|---|---|
setPointerCapture() | 捕获指定指针 |
releasePointerCapture() | 释放指定指针 |
hasPointerCapture() | 判断是否捕获指定指针 |
注意:pointerup 或 pointercancel 后,浏览器会自动释放指针捕获。捕获期间也不会正常触发该指针的 pointerenter、pointerleave 等边界事件。
- 必须考虑 pointercancel
pointerup 和 pointercancel 并不相同。
pointerup:用户正常释放指针。pointercancel:浏览器或系统取消了当前指针操作。
例如,浏览器接管触摸操作进行页面滚动时,可能触发 pointercancel。因此拖拽开发中,通常需要同时监听两个事件进行状态清理。
function stopDrag() { isDragging = false;}
box.addEventListener('pointerup', stopDrag);box.addEventListener('pointercancel', stopDrag);- 多指触摸需要区分 pointerId
Pointer Events 支持多点触控,每个活动指针都有自己的 pointerId。如果只需要单指拖拽,可以记录当前操作的指针 ID,避免其他手指干扰。
let activePointerId = null;
box.addEventListener('pointerdown', (e) => { if (activePointerId !== null) return;
activePointerId = e.pointerId; box.setPointerCapture(e.pointerId);});
box.addEventListener('pointermove', (e) => { if (e.pointerId !== activePointerId) return;
console.log(e.clientX, e.clientY);});
function stopDrag(e) { if (e.pointerId !== activePointerId) return;
activePointerId = null;}
box.addEventListener('pointerup', stopDrag);box.addEventListener('pointercancel', stopDrag);- Pointer 事件可能同时触发 Mouse 事件
为兼容旧代码,浏览器可能在 Pointer 事件之外继续触发对应的 Mouse 事件。例如,一次鼠标点击可能产生以下事件:
pointerdown;mousedown;pointerup;mouseup;click;所以,同一个交互不要同时使用 Pointer 和 Mouse 事件重复处理,否则可能执行两次业务逻辑。
另外,pointerdown 不完全等同于 mousedown:鼠标多个按键同时操作时,pointerdown 只在进入有按键按下的状态时触发。
- pointerenter 和 pointerleave 不支持冒泡
| 事件 | 是否冒泡 |
|---|---|
pointerover | ✅ |
pointerout | ✅ |
pointerenter | ❌ |
pointerleave | ❌ |
pointerdown | ✅ |
pointermove | ✅ |
pointerup | ✅ |
因此,使用事件委托处理指针进入、离开元素时,通常选择 pointerover 和 pointerout,并根据 relatedTarget 判断是否真正跨越了目标元素边界。
总结
| 注意事项 | 推荐做法 |
|---|---|
| 浏览器默认触摸行为干扰 | 使用 touch-action |
| 指针未按下也能触发移动事件 | 判断拖拽状态 |
| 指针离开元素导致事件丢失 | 使用 setPointerCapture() |
| 指针操作可能被取消 | 同时处理 pointercancel |
| 多指触摸相互干扰 | 使用 pointerId 区分 |
| Pointer 与 Mouse 事件可能重复 | 避免重复绑定相同业务逻辑 |
pointerenter/leave 不冒泡 | 事件委托使用 pointerover/out |
实际开发中,最需要掌握的是前三项加上 pointercancel 和 pointerId。 掌握这五点,基本就可以使用 Pointer Events 完成常见的 PC 与移动端拖拽、滑动交互。
Pointer事件的event对象
Pointer 事件触发时,浏览器会向事件处理函数传入一个 PointerEvent 对象,用于获取指针类型、坐标、按键状态、压力等信息。
box.addEventListener('pointerdown', (event) => { console.log(event); console.log(event.pointerType); console.log(event.clientX, event.clientY);});PointerEvent 继承自 MouseEvent,因此既可以使用鼠标事件的属性,也可以使用 Pointer 事件独有的属性。
- event 对象的常用属性
| 属性 | 说明 |
|---|---|
pointerId | 当前指针的唯一标识,用于区分多个活动指针 |
pointerType | 指针设备类型:mouse、touch、pen |
isPrimary | 是否为当前指针类型的主指针 |
clientX / clientY | 指针相对于浏览器视口的坐标 |
pageX / pageY | 指针相对于文档的坐标 |
screenX / screenY | 指针相对于屏幕的坐标 |
offsetX / offsetY | 指针相对于事件目标元素内边距边缘的坐标 |
button | 本次事件中发生状态变化的按键 |
buttons | 当前按下的按键组合 |
pressure | 指针压力,范围为 0~1 |
width / height | 指针接触区域的宽度和高度 |
target | 事件的目标元素 |
currentTarget | 当前正在执行事件监听器的元素 |
type | 当前事件名称,如 pointerdown |
preventDefault() | 阻止可取消事件的默认行为 |
- pointerType:判断指针设备
box.addEventListener('pointerdown', (event) => { if (event.pointerType === 'mouse') { console.log('鼠标操作'); } else if (event.pointerType === 'touch') { console.log('触摸操作'); } else if (event.pointerType === 'pen') { console.log('触控笔操作'); }});通过 pointerType,可以在同一套事件逻辑中区分 PC 和移动端输入设备。
- pointerId 与 isPrimary:区分多点触控
pointerId:区分不同的指针,例如两根手指同时触摸屏幕,会分别对应不同的 ID。isPrimary:判断当前指针是否为该设备类型的主指针。例如,多指触摸时,通常第一根手指为主指针。
box.addEventListener('pointerdown', (event) => { if (!event.isPrimary) return;
console.log('主指针ID:', event.pointerId);});注意:pointerId 不是永久唯一的,指针结束后,其 ID 可能被重新使用。
- 坐标属性
最常用的是 clientX 和 clientY。
box.addEventListener('pointermove', (event) => { console.log(event.clientX, event.clientY); console.log(event.pageX, event.pageY); console.log(event.offsetX, event.offsetY);});拖拽开发通常使用 clientX/clientY 计算移动距离。
- button 与 buttons:判断按键
| 属性 | 常见取值 | 说明 |
|---|---|---|
button | 0 | 主按键(通常是鼠标左键) |
button | 1 | 鼠标中键 |
button | 2 | 次按键(通常是鼠标右键) |
button | -1 | 本次事件没有按键状态变化 |
buttons | 0 | 没有按键按下 |
buttons | 1 | 主按键处于按下状态 |
buttons | 2 | 次按键处于按下状态 |
buttons | 4 | 中键处于按下状态 |
buttons 是位掩码,可以同时表示多个按键。
例如,只在鼠标左键按下时执行:
box.addEventListener('pointermove', (event) => { if (event.pointerType === 'mouse' && event.buttons & 1) { console.log('鼠标左键正在拖动'); }});- 其他属性(了解即可)
这些主要用于触控笔、绘图软件等特殊场景。
| 属性 | 说明 |
|---|---|
tiltX | 触控笔 X 方向倾斜角度 |
tiltY | 触控笔 Y 方向倾斜角度 |
twist | 触控笔的旋转角度 |
tangentialPressure | 触控笔的切向压力 |
altitudeAngle | 触控笔相对于屏幕平面的角度 |
azimuthAngle | 触控笔在屏幕平面上的方向角度 |
这些属性的实际数据取决于硬件支持情况。
总结
对于普通 Web 开发,重点掌握以下属性即可:
| 使用场景 | 重点属性 |
|---|---|
| 判断鼠标、触摸屏、触控笔 | pointerType |
| 单指拖拽、多点触控 | pointerId、isPrimary |
| 获取指针位置、计算拖拽距离 | clientX、clientY |
| 判断鼠标按键 | button、buttons |
| 判断事件目标 | target、currentTarget |
| 绘图、手写板 | pressure、tiltX、tiltY |
学习重点: pointerType、pointerId、clientX/clientY、button/buttons。其他属性根据项目需求再掌握即可。
单指拖拽
<div class="box">拖动我</div>
<style> .box { position: fixed; left: 50px; top: 100px;
width: 120px; height: 120px; background-color: skyblue;
display: flex; justify-content: center; align-items: center;
touch-action: none; user-select: none; cursor: grab; }</style>
<script> const box = document.querySelector('.box');
let startX = 0; let startY = 0; let startLeft = 0; let startTop = 0; let dragging = false; let activePointerId = null;
// 记录当前按下的触摸指针 const activeTouches = new Set();
window.addEventListener( 'pointerdown', (event) => { if (event.pointerType !== 'touch') return;
activeTouches.add(event.pointerId);
// 多指触摸时取消拖拽 if (activeTouches.size > 1) { dragging = false; } }, true, );
box.addEventListener('pointerdown', (event) => { // 鼠标仅允许左键拖拽 if (event.pointerType === 'mouse' && event.button !== 0) return;
// 只允许单指拖拽 if (event.pointerType === 'touch' && activeTouches.size !== 1) return;
if (activePointerId !== null) return;
const rect = box.getBoundingClientRect();
// 记录指针初始坐标 startX = event.clientX; startY = event.clientY;
// 记录元素初始位置 startLeft = rect.left; startTop = rect.top;
dragging = true; activePointerId = event.pointerId;
// 捕获指针 box.setPointerCapture(event.pointerId); });
box.addEventListener('pointermove', (event) => { if (!dragging || event.pointerId !== activePointerId) return;
// 计算指针移动距离 const moveX = event.clientX - startX; const moveY = event.clientY - startY;
// 更新元素位置 box.style.left = `${startLeft + moveX}px`; box.style.top = `${startTop + moveY}px`; });
function stopDrag(event) { if (event.pointerType === 'touch') { activeTouches.delete(event.pointerId); }
if (event.pointerId === activePointerId) { dragging = false; activePointerId = null; } }
window.addEventListener('pointerup', stopDrag); window.addEventListener('pointercancel', stopDrag);</script>文章分享
如果这篇文章对你有帮助,欢迎分享给更多人!














