跳到主要内容

Pointer 指针事件

指针事件(Pointer Events)是浏览器用来统一鼠标、触摸、手写笔的一套抽象。 Konva 把它接了进来,Konva.pointerEventsEnabled 默认就是 true。

shape.on('pointerdown', (e) => {
console.log(e.evt.pointerType); // 'mouse' | 'touch' | 'pen'
});

事件列表​

事件说明
pointerdown按下(鼠标键、手指触屏、笔尖接触)
pointermove移动
pointerup抬起
pointercancel系统中断了这次交互(来电、手势接管)
pointerclick一次完整的点击
pointerdblclick双击
pointerover / pointerout进入 / 离开(冒泡)
pointerenter / pointerleave进入 / 离开(不冒泡)

一次交互会派发两套事件​

这是本页最重要的一点,也是"点一下执行了两遍"最常见的原因。

Konva 内部按 pointerType 把指针事件同时映射成对应的鼠标或触摸事件, 两套都会派发。下面是在真实浏览器里对同一个矩形监听九个事件、 各操作一次测出来的实际顺序:

鼠标点击一次:

pointerdown → mousedown → pointerup → pointerclick → mouseup → click

手指轻点一次:

pointerdown → touchstart → pointerup → pointerclick → touchend → tap

两点结论:

同时监听 pointerdown 和 mousedown 会收到两次。 它们不是"二选一"的关系,而是同一个物理动作的两种表述。 pointerup / mouseup、pointerclick / click 同理。

但 click 和 tap 不会同时触发。 注意上面两行—— 鼠标那行末尾是 click,触摸那行末尾是 tap, pointerclick 根据 pointerType 只映射成其中一个。 所以 on('click tap') 这种写法是安全的,见 移动端 tap 与 click。

选一套用,不要混。 新代码用指针事件(一套代码覆盖三种输入), 或者用 click tap 这类传统写法,但不要两者都注册。

常见问题​

该用 pointer 还是 mouse + touch?​

新代码用 pointer。一套监听覆盖鼠标、触摸、手写笔, 不需要为移动端和桌面端写两份逻辑,也不用担心两者的事件顺序差异。

需要下探到具体设备的场景只有几种: 读取手写笔压感(e.evt.pressure)、 处理多指手势(需要跟踪多个 pointerId)、 或者要精确控制触摸时的滚动行为。

怎么知道是鼠标还是触摸?​

原生事件对象上的 pointerType:

shape.on('pointerdown', (e) => {
if (e.evt.pointerType === 'touch') {
// 手指:命中区域要更大,不要依赖 hover
} else if (e.evt.pointerType === 'pen') {
console.log('压感', e.evt.pressure); // 0 ~ 1
}
});

这比判断 'ontouchstart' in window 可靠得多—— 后者只能判断设备支持触摸,判断不了用户这一次用的是什么。 现在很多笔记本既有触摸屏又有鼠标。

多指触摸怎么区分是哪根手指?​

用 pointerId。每根手指按下时分配一个 id,直到抬起为止保持不变:

const active = new Map();

shape.on('pointerdown', (e) => {
active.set(e.evt.pointerId, { x: e.evt.clientX, y: e.evt.clientY });
});
shape.on('pointerup pointercancel', (e) => {
active.delete(e.evt.pointerId);
});

pointercancel 一定要处理。 系统中断(来电、通知、浏览器接管手势)时 只会派发 pointercancel,不会派发 pointerup。 只监听 pointerup 的话,那根手指会永远留在你的 Map 里, 后续的手势判断全部错乱。

Konva 10.5.0 修过一个相关问题:touchcancel 此前会让 Transformer 保持激活状态,现在取消时会正确派发 pointercancel / touchcancel。

pointerclick 和 click 有区别吗?​

对鼠标操作来说没有——pointerclick 会被映射成 click,两者都会触发。 所以不要两个都监听,否则一次点击处理两遍。

区别在触摸时:pointerclick 映射成 tap 而不是 click。 所以 pointerclick 是"跨设备的点击",click 是"鼠标的点击"。

性能提示​

pointermove 的触发频率很高——高刷新率屏幕上可以到每秒 120 次以上, 而每一次 Konva 都要做一遍命中检测(读取命中图上的一个像素)。 在 pointermove 里做重活是卡顿最常见的来源。

几条实践:

别在 pointermove 里调 getClientRect() 或 find()。 前者要递归遍历子树,后者要遍历整棵场景图。 把结果在 pointerdown 时算好存起来。

拖拽过程中关掉命中检测。 Konva.hitOnDragEnabled 默认就是 false, 拖拽时不做命中检测——这是个好默认值,不要为了"拖的时候也要 hover 效果" 随手打开它,代价比看起来大。

用 Stage.eventBatchFunc() 批量处理。 Konva 10.5.0 新增, 原本是给框架集成层用的,可以把原生输入处理合并到一次批量更新里:

stage.eventBatchFunc((fn) => requestAnimationFrame(fn));

注意它只作用于原生输入事件,直接的 fire() 调用和程序化的属性变更不走批处理。

调大 Konva.dragDistance。 默认 3px,意味着手指抖动 3px 就进入拖拽。 移动端调到 6–10 能减少误触发的拖拽,也就减少了拖拽期间的持续计算。