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

本文介绍了一种在iOS系统中解决touchstart后DOM元素被移除导致touchmove/touchend事件无法正常触发的问题的方法。通过在touchstart时监听touchend/touchmove事件,并在DOM元素被移除时构造伪造的TouchEvent触发到最近的父节点上。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

背景故事

几个月前小编接了一个 全屏阻止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了
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值