先说下为什么使用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里的onClick、onMouseDown、onMouseMove、onMouseUp、onTouchStart、onTouchMove、onTouchEnd进行了重写。
因为我们主要是移动端,所以重点看下onTouchStart、onTouchMove、 onTouchEnd的逻辑:
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 }
}