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

简介: ##背景故事 几个月前小编接了一个 *全屏阻止touch默认行为,并模拟滚动* 的需求。 但事成之后,偶尔出现 “突然锁死” 的问题,无法进行任何滑动。 ![](https://gw.alicdn.com/tfs/TB1QMtbSpXXXXX1XpXXXXXXXXXX-328-318.png_100x100.jpg) ##问题原因 几经排查,是我们设计的 “单指锁

背景故事

几个月前小编接了一个 全屏阻止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了
目录
相关文章
|
JavaScript 前端开发 开发者
判断哪些数据的变化需要触发虚拟 DOM 的更新
判断哪些数据的变化需要触发虚拟 DOM 的更新,需要依据框架的响应式原理、组件的状态管理以及各种用户交互和异步操作等多方面因素。开发者需要深入理解所使用框架的工作机制,合理地组织和管理数据,以确保虚拟 DOM 的更新是高效且必要的。
251 58
|
移动开发 JavaScript 前端开发
VUE实现一个列表清单【props 父子组件通信、slot插槽的使用、全局自定义指令的封装、$nextTick解决异步DOM更新、巧用v-model简化父子组件之间的通信、触发事件的事件源event】
VUE实现一个列表清单【props 父子组件通信、slot插槽的使用、全局自定义指令的封装、$nextTick解决异步DOM更新、巧用v-model简化父子组件之间的通信、触发事件的事件源event】
182 0
|
SQL JavaScript 前端开发
两个相同的负载user在一起启动的时候,造成相关接口调用第一次报异常 调用第二次正常 如此反反复复 解决方法;mysql复习、JavaScript HTML BOM和DOM触发监听机制事件
两个相同的负载user在一起启动的时候 造成相关接口调用第一次报异常 调用第二次正常 如此反反复复 解决方法 放掉一个实例个数
260 0
两个相同的负载user在一起启动的时候,造成相关接口调用第一次报异常 调用第二次正常 如此反反复复 解决方法;mysql复习、JavaScript HTML BOM和DOM触发监听机制事件
|
JavaScript
HTML DOM 允许我们通过触发事件来执行代码。
HTML DOM 允许我们通过触发事件来执行代码。
107 0
|
JavaScript 前端开发
JavaScript 技术篇-js代码触发dom元素绑定事件实例演示,jquery触发元素绑定事件方法
JavaScript 技术篇-js代码触发dom元素绑定事件实例演示,jquery触发元素绑定事件方法
604 0
JavaScript 技术篇-js代码触发dom元素绑定事件实例演示,jquery触发元素绑定事件方法
|
前端开发 JavaScript Unix
h5中performance.timing轻松获取网页各个数据 如dom加载时间 渲染时长 加载完触发时间
在控制台中输入window.performance.timing(html5的属性); 各字段的含义: · navigationStart:当前浏览器窗口的前一个网页关闭,发生unload事件时的Unix毫秒时间戳。
2305 0
|
JavaScript
DOM 节点列表长度(Node List Length)
DOM 节点列表长度(Node List Length)
|
JavaScript
DOM 节点列表长度(Node List Length)
DOM 节点列表长度(Node List Length)
|
JavaScript
HTML DOM 节点树
HTML DOM 节点是指在 HTML 文档对象模型中,文档中的所有内容都被视为节点。整个文档是一个文档节点,每个 HTML 元素是元素节点,元素内的文本是文本节点,属性是属性节点,注释是注释节点。DOM 将文档表示为节点树,节点之间有父子和同胞关系。
|
JavaScript
HTML DOM 节点
HTML DOM(文档对象模型)将HTML文档视为节点树,其中每个部分都是节点:文档本身是文档节点,HTML元素是元素节点,元素内的文本是文本节点,属性是属性节点,注释是注释节点。节点间存在父子及同胞关系,形成层次结构。

热门文章

最新文章