115
汽車大全
【 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的觸發過程
現在我們在touchstart的時候,對 event.target 上添加監聽。監聽 touchend / touchmove 事件
在 touchend / touchmove 觸發的時候,判斷 event.target 還在不在dom樹下
構造一個和 TouchEvent 一模一樣(以假亂真)的 CustomEvent
讓新的 CustomEvent 觸發在原來被移除節點的 parentNode 上
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?
- 安卓4.4+
- Safari 7+
- 安卓 4.3- 請使用 document.creatEvent()
複雜的情況,大塊的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);
總結
現在我們在touchstart的時候,對 event.target 上添加監聽。監聽 touchend / touchmove 事件
存儲當前 e.target 向上追溯到 body 的dom樹的枝條
在 touchend / touchmove 觸發的時候,判斷 event.target 還在不在dom樹下
構造一個和 TouchEvent 一模一樣(以假亂真)的 CustomEvent
計算仍在dom樹上的最深父節點 p
讓新的 CustomEvent 觸發在 p 上
window 現在可以接收到一個冒牌TouchEvent了
最後更新:2017-07-24 11:03:23
上一篇:
H5中基於Canvas實現的高斯模煳
下一篇:
交大法學院與阿裏巴巴安全部簽署網絡社會治理戰略合作協議
阿裏雲推薦碼為什麼用不了?到底還有能用的嘛
GNU通用公共授權(GNU General Public License)中英文版全文
ibatis動態語句中的prepend
劍指Offer之和為S的連續正數序列
android.view.SurfaceHolder$BadSurfaceTypeException: Surface type is SURFACE_
多能互補如何優化?關鍵在於“協調”
Verizon打造數字廣告帝國:收購雅虎或補足短板
蘋果Siri為什麼能成功
吳恩達deeplearning.ai將於11月6日開放第四課,主講卷積神經網絡
在 Linux 上檢測硬盤上的壞道和壞塊