移动端引入 fastclick 导致 antd select 需要双击才能触发

1,535 阅读2分钟

起因

移动端项目中公司框架默认引入了 fastclick.js。因为业务需要,同时引入了 ant-design 中的 select 组件,导致在 iOS 端,select 组件需要双击才能弹出选项。

通过对这个问题进行深入研究,发现是 fastclick 导致的问题。

原因

DOM 事件触发顺序

首选需要了解一下移动端 click,鼠标事件 mouse 以及触摸事件 touch 的触发顺序:

onTouchStart => (onTouchmove) => onTouchEnd => mousedown => (mousemove) => mouseup => click

fastclick 机制

通过查看 fastclick 源码得知:

  1. fastclick 会在 onTouchEnd 中调用了 event.preventDefault() 阻止默认事件(会阻止后续的 mouseclick 事件的触发)。
  2. 并创建并触发自定义的 click 事件(对于原生的 select 元素则触发 mousedown 事件) 通过上述分析可知,如果元素不是原生的 select 组件,则不会触发 mouse 事件。

对于 onTouchStartonTouchEnd 中调用 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 ,导致 dispatchclick 而不是 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+)。

具体内容参考:Making touch scrolling fast by default

在后续的 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 的触发 selectonMouseDown 事件。

如何解决

因为项目不需要兼容老旧的浏览器,并且 <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