阅读115 返回首页    go 汽车大全


【 H5踩坑 】Dom变更引起的 touchend 不触发

背景故事

几个月前小编接了一个 全屏阻止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的触发过程

  1. 现在我们在touchstart的时候,对 event.target 上添加监听。监听 touchend / touchmove 事件

  2. 在 touchend / touchmove 触发的时候,判断 event.target 还在不在dom树下

  3. 构造一个和 TouchEvent 一模一样(以假乱真)的 CustomEvent

  4. 让新的 CustomEvent 触发在原来被移除节点的 parentNode 上

  5. 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?

复杂的情况,大块的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);

总结

  1. 现在我们在touchstart的时候,对 event.target 上添加监听。监听 touchend / touchmove 事件

  2. 存储当前 e.target 向上追溯到 body 的dom树的枝条

  3. 在 touchend / touchmove 触发的时候,判断 event.target 还在不在dom树下

  4. 构造一个和 TouchEvent 一模一样(以假乱真)的 CustomEvent

  5. 计算仍在dom树上的最深父节点 p

  6. 让新的 CustomEvent 触发在 p 上

  7. window 现在可以接收到一个冒牌TouchEvent了

最后更新:2017-07-24 11:03:23

  上一篇:go  H5中基于Canvas实现的高斯模糊
  下一篇:go  交大法学院与阿里巴巴安全部签署网络社会治理战略合作协议