起因
移动端项目中公司框架默认引入了 fastclick.js。因为业务需要,同时引入了 ant-design 中的 select 组件,导致在 iOS 端,select 组件需要双击才能弹出选项。
通过对这个问题进行深入研究,发现是 fastclick 导致的问题。
原因
DOM 事件触发顺序
首选需要了解一下移动端 click,鼠标事件 mouse 以及触摸事件 touch 的触发顺序:
onTouchStart => (onTouchmove) => onTouchEnd => mousedown => (mousemove) => mouseup => click
fastclick 机制
通过查看 fastclick 源码得知:
- fastclick 会在
onTouchEnd中调用了event.preventDefault()阻止默认事件(会阻止后续的mouse、click事件的触发)。 - 并创建并触发自定义的
click事件(对于原生的select元素则触发mousedown事件) 通过上述分析可知,如果元素不是原生的select组件,则不会触发mouse事件。
对于
onTouchStart及onTouchEnd中调用event.preventDefault()阻止的默认事件,可参考:Touch event -- mdn
ant-design select 是如何触发选项弹出的
通过查看 ant-design 使用到的 rc-select 源码,得知是模拟了原生的 select,使用了 mousedown 事件触发弹出选项,但内部并没有使用 select 元素,而是通过 div 元素进行模拟的:
const onInternalMouseDown: React.MouseEventHandler<HTMLDivElement> = (event, ...restArgs) => {
// xxxx
if (onMouseDown) {
onMouseDown(event, ...restArgs);
}
};
// dom 结构
return (
<div
className={mergedClassName}
{...domProps}
ref={containerRef}
onMouseDown={onInternalMouseDown}
onKeyDown={onInternalKeyDown}
onKeyUp={onInternalKeyUp}
onFocus={onContainerFocus}
onBlur={onContainerBlur}
>
{mockFocused && !mergedOpen && (
<span
style={{
width: 0,
height: 0,
display: 'flex',
overflow: 'hidden',
opacity: 0,
}}
aria-live="polite"
>
{/* Merge into one string to make screen reader work as expect */}
{`${mergedRawValue.join(', ')}`}
</span>
)}
{selectorNode}
{arrowNode}
{clearNode}
</div>
)
组件的实现位于
react-component/select中,文件地址:github.com/react-compo…
fastclick 不能识别组件为原生的 select ,导致 dispatch 了 click 而不是 mousedown 事件,进而单击无反应。
为何双击可以触发
通过进一步查看源码,了解到 fastclick 对双击事件进行了特殊处理,当两次点击低于延迟 250ms(fastclick 默认是否为双击判断时间),当双击后会触发 fastclick 对双击事件进行处理。首先,在 onTouchStart 中:
FastClick.prototype.onTouchStart = function(event) {
// xxx
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
event.preventDefault();
}
}
虽然 onTouchStart 中调用了 event.preventDefault(),但是并不能阻止后续事件的触发。移动端为了让滚动能够更快的响应,所以浏览器对于 onTouchStart 事件默认设置 passive: true,即调用 event.preventDefault() 会被忽略(chrome 56+)。
在后续的 onTouchEnd 中,也对双击进行了判断:
FastClick.prototype.onTouchEnd = function(event) {
if ((event.timeStamp - this.lastClickTime) < this.tapDelay) {
this.cancelNextClick = true;
return true;
}
// xxx
}
onTouchEnd 通过 return true 阻止了后续自定义事件的触发,导致后续原生的 mousedown 事件能够触发,进而 ant-design 的触发 select 的 onMouseDown 事件。
如何解决
因为项目不需要兼容老旧的浏览器,并且 <header> 中已经设置了:
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, maximum-scale=1">
项目中不需要 fastclick 来进行兼容,所以最后直接干掉了 fastclick
更多移动端 300ms 解决方案:5 way prevent 300ms click delay mobile devices