閱讀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  交大法學院與阿裏巴巴安全部簽署網絡社會治理戰略合作協議