移动端项目中公司框架默认引入了 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() 阻止默认事件(会阻止后续的 mouseclick 事件的触发)。
  • 并创建并触发自定义的 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 (
          className={mergedClassName}
          {...domProps}
          ref={containerRef}
          onMouseDown={onInternalMouseDown}
          onKeyDown={onInternalKeyDown}
          onKeyUp={onInternalKeyUp}
          onFocus={onContainerFocus}
          onBlur={onContainerBlur}
          {mockFocused && !mergedOpen && (
                  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

    niayyy 前端 @ @ 24.3k
    粉丝