如何用touch模拟click?

1,654 阅读3分钟

先说下为什么使用touch来处理click事件

起因

问题描述:

我的组件以iframe的形式插入到其他页面中,低端机中发现,iframe内元素的click事件失效, 快速点击多次才能触发或者直接失效,而touch事件可以正常触发。

推测:

外部页面对低端机click的300ms延时做了某些处理或拦截,导致click事件无法正常传播进入iframe内的元素。
目前也只是推测,没具体定位到原因,如果有大佬遇到过类似情况,请留言指点~ 感恩~

解决:

由于touch事件可正常触发,决定使用touch事件模拟click事件。

touch事件如何模拟click事件?

我们代理所有的click事件不能一个组件一个组件的去处理,要找一个统一的入口去处理。
由于我使用的是preact,所以去找了react里下载量比较多的react-fastclick源码看了一下

这里把react-fastclick的主要代码精简后梳理了一下:

 var getReactFCInitializer = function (React) {
    return function initializeReactFastclick () {
      var originalCreateElement = React.createElement;
      React.createElement = function () {
        var args = Array.prototype.slice.call(arguments);

        var type = args[0];
        var props = args[1];

        if (type && typeof type === 'string' && (
          (props && typeof props.onClick === 'function') || handleType[type]
        )) {
          args[1] = propsWithFastclickEvents(type, props || {});
        }

        return originalCreateElement.apply(null, args);
      };
    };
  };

可以看出来react-fastclick重写了createElement方法,为了执行propsWithFastclickEvents方法,下面看一下这个方法干了什么

  var propsWithFastclickEvents = function (type, props) {
    var newProps = {};
    for (var key in props) {
      newProps[key] = props[key];
    }

    newProps.onClick = onMouseEvent.bind(null, props.onClick);
    newProps.onMouseDown = onMouseEvent.bind(null, props.onMouseDown);
    newProps.onMouseMove = onMouseEvent.bind(null, props.onMouseMove);
    newProps.onMouseUp = onMouseEvent.bind(null, props.onMouseUp);
    newProps.onTouchStart = onTouchStart.bind(null, props.onTouchStart);
    newProps.onTouchMove = onTouchMove.bind(null, props.onTouchMove);
    newProps.onTouchEnd = onTouchEnd.bind(null, props.onTouchEnd, props.onClick, type);

    if (typeof Object.freeze === 'function') {
      Object.freeze(newProps);
    }
    return newProps;
  };

这个方法里对vnode.props里的onClickonMouseDownonMouseMoveonMouseUponTouchStartonTouchMoveonTouchEnd进行了重写。 因为我们主要是移动端,所以重点看下onTouchStartonTouchMoveonTouchEnd的逻辑:

	var onTouchStart = function (callback, event) {
      // 标记该次事件是否失效
      touchEvents.invalid = false;
      // 标记该次是不是move事件
      touchEvents.moved = false;
      touchEvents.touched = true;
      touchEvents.target = event.target;
      touchEvents.lastTouchDate = new Date().getTime();

      if (typeof callback === 'function') {
        callback(event);
      }
   };
   var onTouchMove = function (callback, event) {
      touchEvents.touched = true;
      touchEvents.lastTouchDate = new Date().getTime();

	// 如果移动量大于阈值,标记该次事件为move事件
      if (Math.abs(touchEvents.downPos.clientX - touchEvents.lastPos.clientX) > MOVE_THRESHOLD ||
        Math.abs(touchEvents.downPos.clientY - touchEvents.lastPos.clientY) > MOVE_THRESHOLD) {
        touchEvents.moved = true;
      }

      if (typeof callback === 'function') {
        callback(event);
      }
   };
   var onTouchEnd = function (callback, onClick, type, event) {
      touchEvents.touched = true;
      touchEvents.lastTouchDate = new Date().getTime();
      
      if (typeof callback === 'function') {
        callback(event);
      }

	// 如果该次事件有效,并且不是move事件
      if (!touchEvents.invalid && !touchEvents.moved) {
        var box = event.currentTarget.getBoundingClientRect();

		// 如果事件坐标位于元素坐标内
        if (touchEvents.lastPos.clientX - (touchEvents.lastPos.radiusX || 0) <= box.right &&
          touchEvents.lastPos.clientX + (touchEvents.lastPos.radiusX || 0) >= box.left &&
          touchEvents.lastPos.clientY - (touchEvents.lastPos.radiusY || 0) <= box.bottom &&
          touchEvents.lastPos.clientY + (touchEvents.lastPos.radiusY || 0) >= box.top) {
			// 如果是可点击状态,触发点击事件
          if (!isDisabled(event.currentTarget)) {
            if (typeof onClick === 'function') {
              onClick(event);
            }

            if (!event.defaultPrevented && handleType[type]) {
              handleType[type](event);
            }
          }
        }
      }
  };

整体模拟逻辑是:

  • 在touchstart时记录事件,设置标记
  • 设置位移阈值,在touchmove时判断位移量,区分move和tap事件
  • 在touchend,判断事件是否有效、是否还在touchstart触发时的元素内,是就触发tap

最终实现

统一入口没有选择,createElement,在preate源码中发现在createVnode时调用了一个options.vnode
这个options是给各种插件预留的入口,配置各种回调,比较适合做我们事件代理的入口。
简易版fastClick

import { options } from 'preact';

const OPTS = {
    threshold: 10
};


let injected, hasTouch;

export default opts => {
    for (let i in opts) {
        if (opts.hasOwnProperty(i)) {
            OPTS[i] = opts[i]
        }
    }
    if (injected) return;
    injected = true;

    // 记录真正的options.vnode方法
    let oldHook = options.vnode;
    // 覆盖
    options.vnode = vnode => {
        // 取出vnode上的props
        let props = vnode.props || vnode.attributes;
        // 遍历属性,如果设置了onTouchTap,对props进行事件代理处理
        if (props) {
            for (let i in props) {
                if (props.hasOwnProperty(i) && i.toLowerCase()==='ontouchtap') {
                    proxy(props);
                    break;
                }
            }
        }
        if (oldHook) oldHook(vnode);
    };
};


function proxy(props) {
    let map = {};
    for (let prop in props) {
        if (prop.hasOwnProperty(prop)) {
            map[i.toLowerCase()] = prop
        }
    }

    // 记录原始方法
    let start = props[map.ontouchstart],
        tap = props[map.ontouchtap],
        click = props[map.onclick];

    props[map.onclick || 'onClick'] = e => {
        if (click) click(e);
        if (!hasTouch) return tap(e);
    };

    // 覆盖touchstart
    props[map.ontouchstart || 'onTouchStart'] = e => {
        let down = coords(e);
        hasTouch = true;

        // 覆盖touchend
        addEventListener('touchend', function onEnd(e) {
            removeEventListener('touchend', onEnd);
            let up = coords(e)
            let dist = Math.sqrt( Math.pow(up.x-down.x,2) + Math.pow(up.y-down.y,2) )
            // 偏移量小于阈值,触发tap事件
            if (dist < OPTS.threshold) {
                tap(e)
            }
        })

        if (start) return start(e)
    }
}


function coords(e) {
    let t = e.changedTouches && e.changedTouches[0] || e.touches && e.touches[0] || e
    return { x: t.pageX, y: t.pageY, target: t.target }
}