如果你在PC端做过长按交互,应该知道一件很麻烦的事:浏览器压根没有原生longpress事件。移动端你还能靠touchstart和touchmove凑合模拟,但到了PC端,面对的是mousedown、mouseup、click这些老朋友,想要在某个div上实现"按住多久才触发某个动作",得自己动手造轮子。
这篇东西我打算完完整整地讲清楚"PC端长按div并处理事件"这件事。不光是给你一个现成代码,还会拆开讲为什么这么写、哪些地方容易踩坑、怎么处理拖拽和文本选中的干扰、怎么在Vue和React里封装成可复用的东西。适合正在做PC端后台系统、自定义右键菜单、长按复制、长按预览这类交互的同学参考,前端新手也能跟着跑通。
1. 先想清楚:PC端的长按到底是什么
1.1 长按不只是"按得久"
很多刚入门的同学以为长按就是"用鼠标按住不放,过了几百毫秒触发一下事件"。这么说没毛病,但实际做起来远比这复杂。长按这个交互在PC端天然会跟三个东西打架:鼠标移动、文本选中、鼠标右键。
先说鼠标移动。你在mousedown之后如果鼠标稍微动了几个像素,到底是算长按还是算拖拽?如果是在一个列表里长按希望弹出操作菜单,用户手一抖鼠标移出去,结果菜单照样弹出来了,体验就会很奇怪。
再说文本选中。PC端浏览器对鼠标按住的默认行为就是选中文本。你按住一个div里的文字不放,除非你明确阻止默认行为,否则浏览器会立刻进入文本选择模式,然后你的"长按"动作就变得乱七八糟。
最后是右键。很多PC端长按的最终目的是唤起自定义菜单,但右键本身就能唤起浏览器菜单。如果你不想让两者冲突,就得拦截contextmenu事件。这些细节堆起来,长按这个"简单动作"就不那么简单了。
1.2 PC端和移动端的差异
移动端的长按通常依赖touchstart、touchend、touchmove,核心原因是移动端没有鼠标事件。但PC端用鼠标,天然有mousedown和mouseup。这看起来更直观,但PC端有个移动端没有的麻烦:鼠标在按住的同时可以到处乱跑,而手指在触摸屏上按下之后通常不会发生位移。
所以PC端模拟长按,本质上就是三个阶段:
- 按下时启动一个计时器;
- 在时长内如果发生"不该发生的事"(移动、松开、离开),就把计时器清掉;
- 计时器走完还没被清掉,就触发长按逻辑。
听起来很简单,但里面全是细节。接下来我先用原生JS写一个最基础版本,让所有人都能理解原理,再做封装。
2. 原生JS实现PC端长按事件
2.1 最小可用例子
咱先不看框架,用纯HTML和JavaScript写一个能跑的例子。假设页面上有一个div,我想让它被长按600毫秒后,在控制台打印一行日志,同时给这个div加一个高亮样式。
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>PC端长按div示例</title> <style> #pressDiv { width: 300px; height: 200px; background: #f0f0f0; border: 1px solid #ccc; display: flex; align-items: center; justify-content: center; font-size: 18px; user-select: none; } #pressDiv.long-pressed { background: #ffe08a; border-color: #d49a00; } </style> </head> <body> <div id="pressDiv">按住我超过600ms试试</div> <script> const div = document.getElementById('pressDiv'); let timer = null; const LONG_PRESS_MS = 600; function startPress() { // 防止多次mousedown启动多个计时器 if (timer) return; timer = setTimeout(() => { console.log('触发了长按事件'); div.classList.add('long-pressed'); timer = null; }, LONG_PRESS_MS); } function cancelPress() { if (timer) { clearTimeout(timer); timer = null; } } div.addEventListener('mousedown', startPress); // 松开鼠标,结束计时 div.addEventListener('mouseup', cancelPress); // 鼠标离开div,也应该取消 div.addEventListener('mouseleave', cancelPress); </script> </body> </html>这个例子跑起来,你按住div超过600毫秒再松开,会发现控制台打印了一次日志;如果按了300毫秒就松开,什么都不会发生。
注意我在CSS里加了user-select: none,这一步很关键。不加的话,你按住鼠标的时候,浏览器会试图选中div里的文字,视觉上出现蓝底高亮,看起来非常出戏。这在长按场景里基本是必加的,除非你的div里压根没有需要选中的内容,或者你希望长按时允许用户复制文字。
提示:
user-select: none只设置这一个元素,不会影响其他区域。如果这个div内部有子元素需要选中,你可以把user-select: none设在触发长按的父级上,再给子元素单独设置user-select: text。
2.2 关键点:为什么用mousedown/mouseup而不是click
我见过有人用click结合setTimeout来模拟长按,思路是:点击时启动计时器,如果计时器到点了还没被取消,就认为是长按。这个思路最大的问题是click本身是由mousedown和mouseup组成的,从按下到释放再到触发click,中间的时间不可控,而且用户按住不放时click根本不会触发,所以你永远等不到"长按计时开始"的时机。
mousedown是鼠标按键按下的瞬间触发,这个时机是长按真正的起点。mouseup是松开瞬间触发,是长按结束的信号。用这两个事件才能精准控制"按住了多少毫秒"。
另外一个细节:mousedown可以区分鼠标左右键的,event.button为0表示左键,为2表示右键。如果你的长按功能打算用左键触发,最好在startPress里加上判断:
div.addEventListener('mousedown', (e) => { if (e.button !== 0) return; // 只响应左键 startPress(); });不加这个判断的话,有时候用户按住鼠标右键,也会触发你的长按逻辑,很多场景下这不是你想要的效果。
2.3 处理边界:移出去怎么办、右键怎么办
上面的最小例子已经包含了mouseleave取消计时器的逻辑,但光有这个还不够。实际场景中还有几种情况需要注意:
按住鼠标左键,移出div,再移回来。这种情况下
mouseleave会触发,计时器被清掉。但鼠标还是按着的,等你再移回div里时,浏览器不会重新触发mousedown(因为按键一直没有松开),所以长按不会重新开始。这其实是合理行为,但如果你希望重新移入时能重新计时,就得在mouseenter时检查鼠标是否仍处于按下状态,这比较麻烦,一般不建议做。按住鼠标左键,移到浏览器窗口外面再松开。此时
mouseup事件大概率不会触发在div上,甚至可能不会触发在document上。如果代码只监听了div的mouseup,计时器就会残留,导致用户啥也没做,几秒后长按事件还是触发了。解决方式是监听document的mouseup,而不是只监听div本身。右键菜单干扰。如果长按的目标是触发自定义菜单,而用户按住的是左键,一般不会触发
contextmenu。但如果用户长按的是右键,浏览器会自动弹原生右键菜单,你的计时器可能已经开始跑了。更稳妥的做法是在mousedown里就过滤掉非左键按键,同时监听contextmenu并阻止默认行为。
我把这些补丁整合一下,给你一个更健壮的版本:
const div = document.getElementById('pressDiv'); let timer = null; const LONG_PRESS_MS = 600; function startPress(e) { if (e.button !== 0) return; // 只处理左键 if (timer) return; div.classList.remove('long-pressed'); timer = setTimeout(() => { console.log('长按触发'); div.classList.add('long-pressed'); timer = null; }, LONG_PRESS_MS); } function cancelPress() { if (timer) { clearTimeout(timer); timer = null; } } div.addEventListener('mousedown', startPress); div.addEventListener('mouseleave', cancelPress); // 鼠标可能在div外松开,所以用document监听mouseup document.addEventListener('mouseup', cancelPress); // 阻止右键菜单,避免长按右键时弹出浏览器菜单 div.addEventListener('contextmenu', (e) => e.preventDefault());这里有个取舍:contextmenu的preventDefault如果写在div上,那么只有在这个div内点击右键才会被阻止。如果整个应用的自定义菜单都基于这一套,也可以把contextmenu监听挂到document上,由你自己实现菜单逻辑。
3. Vue3里的长按指令封装
3.1 封装一个v-longpress指令
原生写法看得差不多了,但放到项目里,总不能每个用到长按的div都写一遍addEventListener、clearTimeout。在Vue3里,最优雅的姿势是封装成一个自定义指令。这样在模板里写一行v-longpress就能用,跟v-model差不多自然。
Vue3的指令钩子常用的是mounted、unmounted,还支持updated。指令的价值在于把事件绑定、定时器清理都封装到内部,组件使用者不需要关心清理逻辑。
下面是一个完整的v-longpress指令实现,我加了延迟参数支持和回调参数传递:
// longpress.js import { directive } from 'vue'; export const vLongpress = directive({ mounted(el, binding) { // 回调函数 const callback = binding.value; // 长按延迟,默认600ms const duration = Number(binding.arg) || 600; if (typeof callback !== 'function') { console.warn('v-longpress 需要接收一个函数作为参数'); return; } let timer = null; const start = (event) => { // 只响应鼠标左键 if (event.button !== 0) return; if (timer) return; el.classList.remove('long-pressed'); timer = setTimeout(() => { // 触发回调,并传回当前事件对象 callback(event); timer = null; }, duration); }; const cancel = () => { if (timer) { clearTimeout(timer); timer = null; } }; // 把cancel挂到el上,方便unmounted时清理 el.__longPressCancel = cancel; el.addEventListener('mousedown', start); el.addEventListener('mouseleave', cancel); document.addEventListener('mouseup', cancel); el.addEventListener('contextmenu', (e) => e.preventDefault()); }, unmounted(el) { // 移除document上的监听,防止内存泄漏 document.removeEventListener('mouseup', el.__longPressCancel); delete el.__longPressCancel; } });使用方式很简单:
<template> <div class="card" v-longpress="handleLongPress" v-longpress:800="handleLongPressWith800ms" > 按住我触发不同延迟 </div> </template> <script setup> import { vLongpress } from './directives/longpress'; function handleLongPress() { console.log('默认600ms触发'); } function handleLongPressWith800ms() { console.log('800ms触发'); } </script>等等,这里有个问题,同一个元素上不能同时用两个v-longpress,因为它们会绑定同一个指令,后者会覆盖前者。如果你需要不同延迟,建议用指令的参数形式v-longpress:800,但是同一个元素上仍然只用一个指令。我上面的模板里写两个是演示错误用法,你别照搬。
3.2 指令中如何绑定/清理事件
指令内部的清理逻辑比原生写法更需要严谨,因为Vue组件随时可能被销毁。如果组件销毁了,mousedown监听还在el上,那倒还好,因为el也跟着被销毁了,垃圾回收能处理。真正危险的是挂到document上的mouseup监听,如果不显式移除,它会一直残留在document上,等下次再触发时,甚至可能访问到已经销毁的el,造成潜在错误。
所以我上面的代码里用el.__longPressCancel保存了cancel函数,在unmounted钩子里通过document.removeEventListener彻底移除。这是很多人容易漏掉的一步。
另外,Vue3的指令还可以通过updated钩子更新回调函数。如果你的长按回调是动态变化的,比如列表组件复用指令时回调变了,旧的binding.value却没更新,会导致一直触发老函数。稳妥一点的做法是在updated里重新绑定:
updated(el, binding) { // 更新el上的回调引用 el.__longPressCallback = binding.value; }然后在start函数里调用el.__longPressCallback(event)。这样就不用每次更新都卸载再重新绑定事件,性能更好。
3.3 嵌套iframe场景的点击穿透问题
这里必须聊一下热词里提到的"vue3嵌套iframe,没办法触发iframe外层div的点击事件"问题。我在实际项目里也遇到过。
有些第三方页面用iframe嵌进来,外层的div包着iframe。你明明在div上加了好好的mousedown监听,结果一旦鼠标移到iframe的区域内,mousedown事件不会冒泡到父级document,你的长按逻辑立刻失灵。这是因为iframe是独立的文档,与父页面有天然的document隔离。
处理办法大概有几种:
- 给
iframe加一层遮罩。在iframe上方叠一个半透明的div,鼠标永远点在遮罩上,事件就能正常触发。遮罩的尺寸需要跟iframe完全重合,并且在不需要长按时要能移开,否则用户没法正常操作iframe里的内容。 - 监听
blur事件。鼠标切入iframe会导致父页面失去焦点,window会触发blur。这时候可以取消正在计时的长按,避免误触发。但这个方法只是"取消",不能让你在iframe区域实现长按。 - 接受现实,在
iframe外部处理。如果长按的核心区域不在iframe里,只是在布局上挨着它,那就明确把可长按区域圈在iframe外面。
我们在封装指令时至少要做第二点:监听window.blur,自动取消计时器。否则用户按着div不松手,然后鼠标快速划进iframe,计时器仍然在走,过一会儿长按突然触发了,就很莫名其妙。给指令加上这个保护:
const onWindowBlur = () => cancel(); window.addEventListener('blur', onWindowBlur); // 在unmounted时也要移除window的blur监听这样处理之后,即使嵌套了iframe,也不会产生"鼠标都走了还触发长按"的诡异现象。
4. React里的长按实现
4.1 useLongPress Hook
React项目通常用Hook来封装逻辑,比自定义指令更"React化"。我们可以写一个useLongPress,接受一个回调函数,返回一组事件属性,把它们展开到目标div上。
// useLongPress.js import { useCallback, useRef, useEffect } from 'react'; export function useLongPress(callback, options = {}) { const { duration = 600, disabled = false } = options; const timerRef = useRef(null); const callbackRef = useRef(callback); // 始终持有最新的callback useEffect(() => { callbackRef.current = callback; }, [callback]); const clearTimer = useCallback(() => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }, []); const start = useCallback((event) => { // 只响应左键 if (event.button !== 0) return; clearTimer(); timerRef.current = setTimeout(() => { timerRef.current = null; callbackRef.current(event); }, duration); }, [duration, clearTimer]); useEffect(() => { if (disabled) clearTimer(); }, [disabled, clearTimer]); // 组件卸载时清理 useEffect(() => clearTimer, [clearTimer]); return { onMouseDown: start, onMouseLeave: clearTimer, onMouseUp: clearTimer, onContextMenu: (e) => e.preventDefault(), }; }然后在组件里这样用:
import { useLongPress } from './useLongPress'; function App() { const longPressProps = useLongPress( (event) => { console.log('长按div触发了', event); }, { duration: 700 } ); return ( <div {...longPressProps} style={{ width: 300, height: 200, background: '#f0f0f0', display: 'flex', alignItems: 'center', justifyContent: 'center', userSelect: 'none', }} > 长按我试试 </div> ); }这里onMouseUp为什么不直接挂在div上,而是通过Hook返回?因为如果用...longPressProps展开,onMouseUp自然会被绑定到div上,但如果鼠标在按住后移出div再松开,div的mouseup不一定触发。React的事件系统虽然会做事件委托,但mouseup的触发目标还是取决于鼠标位置。稳妥一点的做法是让Hook内部自己管理document级的监听,而不是依赖React在目标元素上绑定。我在实际开发中更推荐下面这个版本:
export function useLongPress(callback, options = {}) { const { duration = 600, disabled = false } = options; const timerRef = useRef(null); const callbackRef = useRef(callback); const disabledRef = useRef(disabled); useEffect(() => { callbackRef.current = callback; }, [callback]); useEffect(() => { disabledRef.current = disabled; }, [disabled]); useEffect(() => { const clearTimer = () => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }; const handleMouseDown = (event) => { if (disabledRef.current) return; if (event.button !== 0) return; clearTimer(); timerRef.current = setTimeout(() => { timerRef.current = null; callbackRef.current(event); }, duration); }; // 通过document监听,捕获鼠标在所有位置的松开和移出 const handleMouseUp = () => clearTimer(); const handleMouseLeave = () => clearTimer(); const handleBlur = () => clearTimer(); document.addEventListener('mousedown', handleMouseDown, true); document.addEventListener('mouseup', handleMouseUp, true); document.addEventListener('contextmenu', preventDefault, true); // 为了灵活性,把取消函数暴露出去,供外部绑定onMouseLeave使用 return () => { document.removeEventListener('mousedown', handleMouseDown, true); document.removeEventListener('mouseup', handleMouseUp, true); document.removeEventListener('contextmenu', preventDefault, true); }; }, [duration]); return { onMouseDown: () => {}, onMouseLeave: () => {}, }; }但这样写有个问题,它变成全局监听了,任何位置按下鼠标都会触发你的长按计时,这肯定不是你想要的。所以实际使用中,React版的正确做法分为两步:
- 在目标
div上用onMouseDown启动计时; - 同时在
document上用mouseup清理计时。
为了在Hook里既能给目标绑定mousedown,又能给document绑定mouseup,需要借助ref把目标元素关联起来。
下面是我推荐的完整写法:
import { useCallback, useRef, useEffect } from 'react'; export function useLongPress(callback, options = {}) { const { duration = 600, disabled = false } = options; const elementRef = useRef(null); const timerRef = useRef(null); const callbackRef = useRef(callback); const disabledRef = useRef(disabled); useEffect(() => { callbackRef.current = callback; }, [callback]); useEffect(() => { disabledRef.current = disabled; }, [disabled]); const clearTimer = useCallback(() => { if (timerRef.current) { clearTimeout(timerRef.current); timerRef.current = null; } }, []); useEffect(() => { const node = elementRef.current; if (!node) return; const handleMouseDown = (event) => { if (disabledRef.current) return; if (event.button !== 0) return; clearTimer(); timerRef.current = setTimeout(() => { timerRef.current = null; callbackRef.current(event); }, duration); }; const handleDocumentMouseUp = () => clearTimer(); const handleMouseLeave = () => clearTimer(); node.addEventListener('mousedown', handleMouseDown); node.addEventListener('mouseleave', handleMouseLeave); document.addEventListener('mouseup', handleDocumentMouseUp); return () => { node.removeEventListener('mousedown', handleMouseDown); node.removeEventListener('mouseleave', handleMouseLeave); document.removeEventListener('mouseup', handleDocumentMouseUp); clearTimer(); }; }, [duration, clearTimer]); return { ref: elementRef, onContextMenu: useCallback((e) => e.preventDefault(), []), }; }然后使用:
function App() { const { ref, onContextMenu } = useLongPress((event) => { console.log('长按了', event.target); }, { duration: 800 }); return ( <div ref={ref} onContextMenu={onContextMenu} style={{ userSelect: 'none' }} > 长按我 </div> ); }这样,mousedown绑定在目标元素上,mouseup挂到document上,mouseleave也绑在目标上。组件卸载时,通过useEffect的清理函数把三个监听全部移除,计时器也会被清掉,不会内存泄漏。
4.2 参数、依赖、注意事项
这个Hook的第二个参数options里可以配置duration和disabled。disabled很实用,比如长按操作只在特定状态下可用,你不需要在组件里做条件渲染,直接通过开关控制。
要注意useEffect的依赖数组里我写了duration和clearTimer,意思是duration变化时会重新绑定事件。经验告诉我,大部分场景下duration是固定值,所以这个重新绑定几乎不会发生。但如果你把duration设计成动态可调的,重新绑定时旧的事件会先被清理,不会产生重复监听。
另外一个容易踩的坑是:callback如果不稳定,比如每次渲染都生成一个新的内联函数,callbackRef.current会不断更新,这没问题,因为我们通过callbackRef来保存最新值,不会导致事件重新绑定。但是如果你偷懒直接在useEffect依赖里写上callback,那每次渲染都会解绑再绑一遍,性能没大事,但代码阅读起来难受,而且可能在快速点击时出现事件丢失。所以尽量用ref保持最新回调,别在依赖数组里加函数。
React StrictMode在开发模式会执行两次useEffect挂载和卸载,如果清理逻辑不干净,会出现事件重复绑定。上面的Hook写好了完整的清理函数,所以StrictMode也没问题。
5. 常见问题与排查实录
5.1 长按触发了click,如何避免
PC端最常见的问题:长按结束之后,紧接着浏览器还会派发一个click事件。如果你的div上同时绑了长按回调和点击回调,就会出现"长按触发了,点击也触发了"的双重问题。
解决办法取决于你的业务:
- 如果点击回调本就和长按逻辑互斥,那么你可以在长按触发时打一个标记,然后在
click回调里判断这个标记,如果标记存在就忽略这次点击,并在下次宏任务中清除标记。 - 如果长按不需要响应
click,你可以更粗暴一点:在longpress触发后给目标元素临时添加一个pointer-events: none,让它短时间内无法接收点击事件,或者直接用一个flag判断。
let longPressTriggered = false; div.addEventListener('mousedown', (e) => { timer = setTimeout(() => { longPressTriggered = true; // 长按逻辑 }, 600); }); div.addEventListener('click', (e) => { if (longPressTriggered) { // 清除标记,并忽略本次点击 longPressTriggered = false; e.preventDefault(); e.stopPropagation(); return; } // 正常点击逻辑 });注意e.preventDefault()在click里阻止不了click的默认行为,但可以阻止一些浏览器行为。实际上你靠longPressTriggered标志位忽略逻辑就够了。这个坑我踩过好几次,有时候长按弹菜单后,菜单外的遮罩监听了点击事件想要关闭,结果点击遮罩的同时触发了长按目标上的click,导致菜单关了又打开,很鬼畜。后来我在菜单打开时设置全局标记,所有click在标记打开时先检查来源,才算彻底解决。
5.2 长按后还触发了拖拽/选中文本
文本选中问题我前面提过,user-select: none要在触发长按的div上设置。但如果你用了浏览器的拖拽API,或者目标div内部有img元素,按住拖动会导致原生拖拽,也会干扰长按。
img默认可拖拽,按住图片不松手,拖到别的浏览器窗口边缘,甚至会把图片拖出页面,长按计时器也被打断。处理方式是在mousedown启动计时时,调用e.preventDefault(),阻止默认行为。preventDefault能阻止文本选中和图片拖拽,但要注意这会影响div内所有文字的选择,如果长按区域里有需要复制的文本,就会变得没法选中。这属于交互冲突,你需要产品上做出取舍。
我的建议是,将长按的交互区和文本选择区分离。比如卡片div本身可长按,但里面的详情文案区允许选择,那就给文案区单独设置user-select: text,并在mousedown时判断事件目标是不是文案区,如果是从文案区按下,就不启动长按计时。
div.addEventListener('mousedown', (e) => { if (e.target.closest('.selectable-text')) { cancelPress(); return; } startPress(e); });5.3 计时器不准确的坑
有些同学用Date.now()在mousedown时记录开始时间,在mouseup时计算差值来判断是否长按。这看起来挺科学,其实有个问题:如果用户一直按着不放,mouseup永远不会触发,你就无法判断到底算不算长按。所以最终还是要靠setTimeout在目标时间点主动触发。
setTimeout本身会受浏览器标签页后台运行、CPU负载等影响,但长按场景的精度通常在几十毫秒级,要求没那么苛刻。真正要注意的是不要重复计时:如果mousedown被多次触发(比如鼠标奇奇怪怪的抖动导致mousedown冒泡重复),就会设置多个定时器,导致长按被触发多次。所以我在所有实现里都加了if (timer) return的保护。
另外,setTimeout的回调里如果访问了event对象,在异步回调执行时,原生event对象很可能已经被浏览器回收或对象属性不可用,尤其是那些需要读取坐标的。React的合成事件也有这个问题。所以我的建议是在mousedown触发setTimeout之前,先把需要的数据(比如clientX、clientY、target)拷贝一份,再在回调里使用。省得调试时发现回调里拿到的坐标是0、0。
5.4 移动端兼容性补丁
虽然标题是PC端,但现在很多项目是响应式,同一个组件可能要在平板或手机上运行。移动端没有mousedown,但有touchstart。干脆做一层兼容,让useLongPress在触摸设备上也能用:
- 在
touchstart里启动计时; - 在
touchmove里判断位移超过阈值就取消; - 在
touchend里取消计时。
但touch事件还有个特有的东西:触摸滚动。用户长按时手指稍微移动一下,页面可能就滚动了。要阻止滚动,可以在touchmove里调用preventDefault,但这会让页面局部滚动失效,所以需要看清楚场景。如果长按区域不需要滚动,可以放心阻止;如果长按区域在可滚动列表里,建议用位移阈值判断,小于10像素就算原地长按,大于10像素就取消,这样既不影响滚动,也不至于误触发。
6. 一些实战心得
做了几年前端,我发现"长按"这种交互在PC端往往被当成小功能处理,但它牵扯到鼠标事件、冒泡、默认行为、组件生命周期、甚至跨文档场景。越是看似简单的功能,越要在工程化封装时把边界条件考虑清楚。我个人比较推荐的做法是整个项目统一封装一个指令或Hook,禁止业务代码里到处写setTimeout。
分享一个小技巧:在长按开始时,可以给div加一个轻微的视觉反馈,比如背景变淡或者加一个环形进度。视觉反馈不仅能提升体验,还能帮你调试——按住多久触发、是否取消了,一眼就能看出来。很多大厂的后台系统里长按操作都会配一个进度环,原理其实很简单:CSS过渡动画就够,不需要引入canvas。
最后提醒一下,长按的延迟时间建议做成可配置项,不要全线用同一个值。工具栏上的长按和列表项上的长按,用户心理预期完全不同。一般列表项600到700毫秒比较合适,图标上的长按可以缩短到400毫秒。这个没有绝对标准,多找几个人试一试最靠谱。