如果你关注我应该知道,我最近对PC端页面进行移动适配。在这个过程中,为了节省用户300ms的时间,同时给予用户更及时的点击反馈(这意味着更好的用户体验),我在尝试使用移动端独有的 touchstart
事件替代传统的 click
事件,这过程中我遇到了一些小问题,并成功解决了,你可以通过这篇文章查看具体的情况。
所谓祸不单行,在即将发布上线的时候,我又突然发现使用 touchstart
事件后,移动设备上出现了另一个比较诡异的现象:当用户滚动页面后,原本绑定在fixed定位的navbar元素上的 touchstart
事件会时常失效。你可以通过扫描下方二维码,并使用你的Safari浏览器或Chrome浏览器(注意:不是浏览器自带的模拟器)亲自感受这一奇怪的现象。
当然,最终我成功解决了这个问题,并且有意思的是,这个问题似乎并不出自我的代码,而被我归咎于是浏览器的Bug。但是对于这个Bug出现的原理,我也只有一个大概的推测,如果你清楚的知道产生这一现象的原因,也欢迎你和我分享。
在本篇文章中,我不但会记录我的解决方案,并且会记录我在遇到这个问题后的debug的过程与分析思路。不过如果你正被这个问题搞得焦头烂额,只想快点摆脱这个问题,你可以直接翻阅到文章底部“解决方案”部分,参考我的解决方案(我真是贴心,对吧? ?)。
一 · 问题描述
- 移动设备:iPhone 6
- 操作系统:iOS 11.2.5
- 测试浏览器:Safari,Chrome
- 点击此处查看示例代码
当在移动设备上使用测试浏览器打开网页并滚动屏幕后,会发现再次点击navbar,navbar元素绑定的 touchstart
事件并没有被触发(或偶尔不被触发)。
二 · 分析排查
(一)步骤一:使用搜索引擎
我是在无意中发现该问题的,当时观察到的现象是绑定在navbar上的 touchstart
事件有时会被触发,有时会失效。于是我使用Google搜索了“touchstart 事件偶尔失效”的关键字,很遗憾,并没有什么靠谱的答案。这说明“并不存在touchstart偶尔失效的问题”。也就是说,我需要找到确切的令 touchstart
响应事件失效的原因。
接下来,我不断的在移动设备上尝试各种操作(双击,滚动屏幕,放大等),并留心移动设备的响应,最终将 touchstart
绑定事件失效的原因确定为在“页面滚动之后”。接下来的事很简单,继续Google搜索“touchstart 事件在页面滚动后失效”。观察首屏搜索结果,并点击进去查看,遗憾的是,并没有什么合适的信息:
接着我抱着试试看的态度切换百度搜索同样的关键词,还不错,已经有了一个相似度很高的搜索结果,但是阅读后发现依然不是我想要的:
是时候上最终的大杀器了,使用英文关键字搜索!以下是我使用了“touch event doesn‘t respond after page scroll”关键字的Google搜索结果:
老样子,还是没有令人满意的结果。至此我获得了两点信息:
- 可能是我的代码写的有问题:因为很小可能出现奇怪的现象从来没有人遇到过;
- 我应该从
touch
事件的相关概念上找原因;
(二)步骤二:隔离代码,明晰概念
至此,debug的第一阶段结束,我的徒劳无功表明了这个问题并不简单,我需要认真对待,接下来我采用了以下两种方法:
- 抽离核心代码,通过隔离外界不确定因素,排除外因;
- 查阅MDN上touch事件的文档,查找可能引发此类问题的内因;
思路很清晰对吧,但是我并没有在相关文档中找到可能引发此问题的任何灵感。不过好在我通过第一步已经让问题变得非常清晰了,不要灰心,继续思考。
(三)步骤三:大胆假设,小心求证
基本上到了这个阶段,debug的过程就进入到经验和直觉领域了,要成功解决这个问题,你有时还需要一点点运气,我在这个过程中尝试了以下方案:
“无脑试对”:我将在搜索引擎中看到的一些问题的解决方法,逐个试验,希望有个会管用,我可以获得更多信息去定位问题出现的原因。这些尝试有:为事件的回调函数添加 e.preventDefault()
方法,替换 touchstart
事件为 touchend
事件或者直接是 click
事件。
很尴尬,这些尝试都没有起到作用,问题依然存在,不过没有关系,我本来也没有对这个简短的尝试抱太大的期望,不过这其实也说明这个奇怪的现象和touch具体的事件类型无关,和touch事件误触发其余事件无关。
目前为止,我已经知道了 touch
事件我使用的方式是正确的,并且没有其余的因素可以干扰点击事件的触发,自然而然的,我开始好奇,浏览器到底有没有检测到我手指的“点击”。这可以通过以下代码得到答案:
window.addEventListener("touchstart", e => {
console.log(e.target)
})
奇妙的事情发生了,我的navbar居然不再出现页面滚动后touch事件失效的问题!但是当我按照相同的思路,将代码替换为下面的代码想要看看返回值时:
var navbar = document.querySelector(".navbar")
navbar.addEventListener("touchstart", e => {
console.log(e.target)
})
问题又出现了,并且当页面滚动后,每当我再次点击navbar,控制台没有任何输出,这意味着浏览器认为我并没有点击navbar!
这不科学,但是我已然看到胜利的曙光。当我我将原先绑定在navbar上的 touchstart
事件通过事件的冒泡机制绑定在 window
对象,通过判断 e.target
属性进行事件回调时 -- 问题解决了,页面正常了,整个世界都清净了...
最终的解决方案
代码如下:
var navbar = document.querySelector(".navbar")
window.addEventListener("touchstart", e => {
if (e.target === navbar) {
// callback
}
})
扫描二维码查看正确的效果:
到此为止问题被成功解决了吗?并没有。
虽然世界清净了的那一刻令人神清气爽,但是这只是需求被实现了,问题并没有被解决,我指的是我心里的那个问题:“为什么这样就行,而原来那样就不行?”。这个问题至关重要,也希望你们不要忽略。
你同意吗?那让我们继续。
让我们再回过头分析一下我们的代码,很明显它已经非常精简了,唯一可能出问题的地方在于我们给navbar的 fixed
定位。我们再想想我们是怎么“误打误撞”解决这个问题的,navbar检测不到我们的点击,但是window可以,将这两个线索放在一起思考,我得出了一个很值得怀疑的对象:层级。
我试着取消了navbar的 position: fixed;
声明,果然,一切又都正常了。看来这一奇异现象的始作俑者就是这条声明。而我能想到与之相关的因素就是层级,我是指DOM对象的层级。
最终我是这样解释这个奇怪现象产生的原因:
在页面初始化时,浏览器的DOM树被正确的渲染,也就是说DOM元素间的关系正确,因此 navbar 元素可以准确的捕捉我们的 touchstart 事件,但是当页面滚动后,浏览器丢失了 navbar 元素的层级关系, touchstart 事件无法通过冒泡被 navbar 元素捕捉,因此我们绑定的事件没有响应。而当我们让整个window对象监测 touchstart 事件后,浏览器可以重新正确的计算DOM对象间的关系,navbar 层可以捕捉到冒泡的事件,因此一切就都正常了。
这个解释有说服你吗?其实我心里也没个底,毕竟这只是我基于现象的一种推测。但无论如何,这种奇怪现象的发生,应该被归咎于浏览器,而不是我的代码(哈,松了一口气)。
到这里,这个问题就结束了吗?并没有。如果只是凭一个现象,一个推测就甩锅浏览器,会不会让人有一种钦定的感觉,让某些人不服呢?会的,我自己心就比较虚,不过没关系,只要掌握了以下的关键诀窍,甩锅浏览器还不是分分钟的事。
该诀窍就是 -- 你自己去多测几个浏览器啊,朋友!!
我们根据引擎区分不同浏览器,
- 使用Webkit引擎的浏览器:Chrome,Safari
- 使用Gecko引擎的浏览器:Firefox
- 使用Presto引擎的浏览器:Opera
于是我下载了Firefox浏览器重新测试了原代码下页面效果,果然没有问题!
呵呵,不是我代码的问题。都是使用Webkit引擎的浏览器不好 :)。
三 · 总结
你以为本篇文章就这么结束了?并没有(失望吧),实际上写到这里,你应该也有所感觉,虽然这次debug成功解决了问题,但整个过程并不流畅高效,并且在其中走了些许弯路。结合本次debug的经验,我总结了以下几个在下次debug过程中需要注意的方面:
- 依次使用Google,Google(英文关键字),百度搜索引擎搜索问题关键字;
- 对没有搜索到结果的问题保持警惕(检查自己的代码);
- 编写Demo,隔离核心代码,简化分析背景;
- 确保掌握相关技术知识点;
- 首先使用各浏览器测试(这样便可以尽早排除是否是浏览器Bug);
- 尽早使用
debugger
关键字而不是console.log
方法进行调试;(没错,debugger
关键字让调试更高效); - 仔细观察问题,大胆假设,小心求证;
- 不要放弃,在解决需求后,要解决问题;
- 如果有时间精力的话,将发现的问题和debug过程中学到的知识总结一篇博客吧 :);
以上,是我在实际开发过程中发现问题,分析问题并解决问题的过程和思路,希望对你们有帮助。