【 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了
时间: 2024-11-01 17:29:50

【 H5踩坑 】Dom变更引起的 touchend 不触发的相关文章

【踩坑记】从HybridApp到ReactNative

前言 随着移动互联网的兴起,Webapp开始大行其道.大概在15年下半年的时候我接触到了HybridApp.因为当时还没毕业嘛,所以并不清楚自己未来的方向,所以就投入了HybridApp的怀抱. HybridApp最早好像是国外的PhoneGap,然后国内有AppCan.Dcloud.APICloud等等.我当时接触的是APICloud,相比于其他平台,APICloud最大的特点是它的混合程度比较高! 要知道,Webapp最大的问题就是性能问题始终无法和原生App相比,由此才发展出来Hybrid

经典推送:针对jQuery升级踩坑大全

背景 jQuery想必各个web工程师都再熟悉不过了,不过现如今很多网站还采用了很古老的jQuery版本.其实如果早期版本使用不当,可能会有 DOMXSS漏洞,非常建议升级到jQuery 1.9.x或以上版本.前段时间我就主导了这件事情,把公司里我们组负责的项目jQuery版本从1.4.2升级到了jQuery 1.11.3.jQuery官方也为类似升级工作提供了jQuery Migrate插件. 言归正传. 坑从何处来 jQuery 1.11.3是1.x时代的最后一个版本(作者更新:2016年1

曾经踩坑党,如今护航忙 | 袋鼠云的双11故事之一

普通人提起双11,谈的都是剁手党 袋鼠云提起双11,谈的却是踩坑党 每年双11,同样的通宵达旦.同样的激动万分.同样的心跳加速,同样的肾上腺素增加,不一样的是:剁手党在Happy,踩坑党在忧虑. 这个双11,袋鼠小妹采访了曾经参与过阿里双11的几位袋鼠云技术专家,为大家分享他们别样的双11故事.他们分别是袋鼠云首席大数据架构师申杭.首席数据库架构师俊达(大家尊称:达叔),首席运维专家留良.首席售后服务专家南晨.(恩,都是首席,Teamleader级别) 袋鼠小妹有故事,那你准备好酒了么? ---

SQL Server在AlwaysOn中使用内存表的“踩坑”记录

前言 最近因为线上alwayson环境的一个数据库上使用内存表.经过大概一个星期监控程序发现了一个非常严重问题这个数据库的日志文件不会截断,已用空间一直在增加(存在定时的每个小时的日志备份),同时内存表数据库文件也无法删除,下面就介绍一下后面我的处理过程,话不多说了,来一起看看详细的介绍吧. 数据库:SQL Server2014 Enterprise Edition (64-bit) 删除文件 使用一个单独非alwayson环境的数据库测试. 一.创建内存表 ---创建内存表文件组 ALTER

秦苍科技是如何管理数百个微服务并避免踩坑的?

[编者的话]过去两年中,微服务架构是一个非常热门的技术名词.秦苍科技也在微服务方面做了大量的投资和实践,我们有开发.测试.准生产.生产四套环境,每套环境有230+个微服务,总共有近1000个微服务. 本文讲的是秦苍科技是如何管理数百个微服务并避免踩坑的?秦苍科技为什么要采用微服务的架构?如何管理这么多微服务?本文将对这些问题进行阐述,希望对正在踩坑路上和即将踩坑的朋友们有所帮助. 为什么要使用微服务 关于微服务架构优点有很多讨论.但是,个人认为许多优点都可以算作一些"伪优点".例如:

【踩坑经历】一次Asp.NET小网站部署踩坑和解决经历

2013年给1个大学的小客户部署过一个小型的Asp.NET网站,非常小,用的sqlite数据库,今年人家说要换台服务器,要重新部署一下,好吧,虽然早就过了服务时间,但无奈谁叫人家是客户了,二话不说,上,源代码和以前的文件都有,部署还不是分分钟的事情,打开IIS挂上去就行了.谁知道,这个部署将近花了2天的时间.看看踩坑过程和解决方法. 本文原文地址:http://www.cnblogs.com/asxinyu/p/4380380.html 回来一看,9个反对,我心痛啊,这些童鞋,你们觉得这篇文章哪

JavaScript 踩坑心得— 为了高速(下)

一.前言 本文的上一篇 JavaScript 踩坑心得- 为了高速(上) 主要和大家分享的是 JavaScript 使用过程中的基本原则以及编写过程中的心得分享,本文主要和大家聊聊在各个使用场景下的 JavaScript 使用,以及在性能优化方面的优化经验等 二.各种场景下的 JavaScript 1.用于 UI 应用的 JavaScript 与大多数服务器端语言一样,用于客户端应用的 JavaScript 框架从来就不缺少.然而,和用在后端应用与服务中一样,笔者偏好使用较小的模块,将这些小模块

Android Studio踩坑记

拾起Android项目,需要使用Goolgle Play Services.顺应潮流换了Android Studio,开启了踩坑之旅. 尝试直接将Eclipse项目导入AS,结果根本没法用啊.正确的方法应该是升级ADT,在Eclipse下导出build.gradle然后再导入.但是升级的时间还不如直接新建项目把资源拷进去,同时也能了解一下AS默认的项目结构. 第一个遇到的问题是新建的项目没有assert和lib目录.java和res等资源都在src/main目录下,于是我将assets和libs

JavaScript 踩坑心得— 为了高速(上)

一.前言 很多情况下,产品的设计与开发人员一直想打造一套高品质的解决方案,从而快速.平稳地适应产品迭代.速度是衡量产品适应性的真正且唯一的标准,而且,这并不是笔者的一家之言. 「速度是衡量适应能力的真正指标.」 --艾瑞克·埃利奥特 许多公司选择 JavaScript,就是看中了它灵活.快速的优点.尽管此言非虚,但如果你在构建 JavaScript 系统时考虑得不够周全,灵活与高速的特性反而可能将你带入歧途. 一些值得特别关注的问题包括: 代码重复 样式或风格不一致 无法随意扩展 工具与模块选择