背景故事
几个月前小编接了一个 全屏阻止touch默认行为,并模拟滚动 的需求。
但事成之后,偶尔出现 “突然锁死” 的问题,无法进行任何滑动。
问题原因
几经排查,是我们设计的 “单指锁定” 模块引起的。
- 为了更好的体验,我们做了一个“单指锁定”模块,当上一个手指不放开时,另一个手指不论怎么滑也不会引起交互。
- 因此如果 由于某种情况 导致 touchend 丢失,就无法解除当前手指的锁定状态,导致锁死。
某种情况是什么情况?
原因1 · iOS 底部控制中心划出引起的浏览器JS阻塞
图1 万恶的控制中心,弹出时会阻塞JS
原来如此
通过 window上的 touchcancel 事件来监听这一状况
window.addEventListener('touchcancel',e=>{
// ... 重设触摸状态
});
然后重设触摸状态,简简单单就解决了这个问题。然后十分钟后
看来事情并不简单。
回顾一下touch事件触发机制
我们先来回顾一下dom的事件传递机制
图2 事件沿dom树传递
Touch事件比较特殊,它有个特点
如果你 touchstart 在 div.c 上,接下来的 touchmove / touchend 全部都会一直触发在 div.c 上
那么问题就来了
如果我在 touchstart 之后把 e.target 移除,会发生什么事呢?
图3 一脸懵逼温抖君
于是乎,我们的 window 除了首次 touchstart 的响应。
对于后续 持续触发在 div.c 上的 touchmove/touchend ,完全无法传递
解决方案
缺什么补什么,这条线由我们来牵。
图4 自定义事件模拟原有的TouchEvent的触发过程
- 现在我们在touchstart的时候,对 event.target 上添加监听。监听 touchend / touchmove 事件
- 在 touchend / touchmove 触发的时候,判断 event.target 还在不在dom树下
- 构造一个和 TouchEvent 一模一样(以假乱真)的 CustomEvent
- 让新的 CustomEvent 触发在原来被移除节点的 parentNode 上
- window 现在可以接收到一个冒牌TouchEvent了
talk is cheap.
//window上监听事件
window.addEventListener('touchstart', e => {
const t = e.target;
//事件handle
const moveHandle = function (e) {
//判断节点在不在Dom树下
if (!inBody(e.target)) {
//触发伪造的自定义事件
dispatchFakeEvent(e);
}
};
const endHandle = function (e) {
//判断节点在不在Dom树下
if (!inBody(e.target)) {
//触发伪造的自定义事件
dispatchFakeEvent(e);
}
//移除监听
t.removeEventListener('touchmove', moveHandle);
t.removeEventListener('touchend', endHandle);
};
//监听事件
t.addEventListener('touchmove', moveHandle);
t.addEventListener('touchend', endHandle);
}, true);
怎么判断在不在dom树上?
/**
* 判断节点是否在body下
* ------------------------
* @param node
* @return {Boolean}
*/
function inBody(node) {
return (node === document.documentElement) || (node === document.body) ? false : document.body.contains(node);
}
怎么伪造TouchEvent?
//创建同名自定义事件
const E = new CustomEvent(event.type, {
bubbles: true,
});
//拷贝参数
E.changedTouches = event.changedTouches;
E.targetTouches = event.targetTouches;
//触发事件
node.dispatchEvent(E);
兼容性 CanIUse?
- 安卓4.4+
- Safari 7+
- 安卓 4.3- 请使用 document.creatEvent()
复杂的情况,大块的DOM变更
有时候我们一删就是一大片dom,那很可能 event.target.parentNode 也一起被从Dom中移除了。
怎么办呢?
图5 向上搜索,寻找仍在dom树上的最深父节点
在touchstart的时候,先把此时的 e.target 这一条树枝存起来。
这样在后续判断时,就可以向上搜索,寻找仍在dom树上的最深父节点。
talk is cheap.
window.addEventListener('touchstart', e => {
const t = e.target;
//计算元素初始dom树枝
let n = t;
const tree = [t];
while (n.parentNode && n !== document.documentElement) {
tree.push(n.parentNode);
n = n.parentNode;
}
//.....
});
/**
* 获取仍在dom树上的最深父节点
* -------------------------
* @param tree
@return {}
*/
function getDomWhichOutsideBody(tree) {
let n = tree[0];
while (n.parentNode !== null) {
n = n.parentNode;
}
let i = tree.indexOf(n);
return i > -1 ? tree[i + 1] : null;
}
修改后的伪造函数
/**
* 伪造的Touch事件并触发
* ------------------------
* @param event
* @param tree
*/
function dispatchFakeEvent(event, tree) {
//获取仍在dom树上的最深父节点 , 若节点不存在则直接返回
const p = getDomWhichOutsideBody(tree);
if (!p)return;
//创建同名自定义事件
const E = new CustomEvent(event.type, {
bubbles: true,
});
//拷贝参数
E.changedTouches = event.changedTouches;
E.targetTouches = event.targetTouches;
//触发事件
p.dispatchEvent(E);
}
最后
完整源码
/**
* @fileOverview
* iOS系统中, 如果 在touchstart 中将 event.target 从Dom树上移除,
* 则后续的 touchmove / touchend 均无法传递到 其原有父级元素上
*
* 此补丁通过在 touchstart 时,在 e.target 上添加监听 move/end
* 随后判断此元素是否被移除,
* 如果被移除,则在该元素曾在dom树上的最底层节点上,触发对应事件来达到事件沿dom树冒泡的效果
*
* @author iNahoo
* @since 2017/7/13.
*/
"use strict";
/**
* 判断节点是否在body下
* ------------------------
* @param node
* @return {Boolean}
*/
function inBody(node) {
return (node === document.documentElement) || (node === document.body) ? false : document.body.contains(node);
}
/**
* 获取仍在dom树上的最深父节点
* -------------------------
* @param tree
@return {}
*/
function getDomWhichOutsideBody(tree) {
let n = tree[0];
while (n.parentNode !== null) {
n = n.parentNode;
}
let i = tree.indexOf(n);
return i > -1 ? tree[i + 1] : null;
}
/**
* 伪造的Touch事件并触发
* ------------------------
* @param event
* @param tree
*/
function dispatchFakeEvent(event, tree) {
//获取仍在dom树上的最深父节点 , 若节点不存在则直接返回
const p = getDomWhichOutsideBody(tree);
if (!p)return;
//创建同名自定义事件
const E = new CustomEvent(event.type, {
bubbles: true,
});
//拷贝参数
E.changedTouches = event.changedTouches;
E.targetTouches = event.targetTouches;
//触发事件
p.dispatchEvent(E);
}
//监听事件
window.addEventListener('touchstart', e => {
const t = e.target;
/**
* 计算元素初始dom树
* -----------------
* PS: 我总觉得这么做不太稳妥。
*/
let n = t;
const tree = [t];
while (n.parentNode && n !== document.documentElement) {
tree.push(n.parentNode);
n = n.parentNode;
}
//事件handle
const moveHandle = function (e) {
//判断节点在不在Dom树下
if (!inBody(e.target)) {
dispatchFakeEvent(e, tree);
}
};
const endHandle = function (e) {
//判断节点在不在Dom树下
if (!inBody(e.target)) {
dispatchFakeEvent(e, tree);
}
//移除监听
t.removeEventListener('touchmove', moveHandle);
t.removeEventListener('touchend', endHandle);
};
//绑定事件
t.addEventListener('touchmove', moveHandle);
t.addEventListener('touchend', endHandle);
}, true);
总结
- 现在我们在touchstart的时候,对 event.target 上添加监听。监听 touchend / touchmove 事件
- 存储当前 e.target 向上追溯到 body 的dom树的枝条
- 在 touchend / touchmove 触发的时候,判断 event.target 还在不在dom树下
- 构造一个和 TouchEvent 一模一样(以假乱真)的 CustomEvent
- 计算仍在dom树上的最深父节点 p
- 让新的 CustomEvent 触发在 p 上
- window 现在可以接收到一个冒牌TouchEvent了
时间: 2024-11-01 17:29:50