这篇文章发布于 2021年11月24日,星期三,00:56,归类于 JS API。 阅读 24257 次, 今日 18 次 10 条评论
by zhangxinxu from https://www.zhangxinxu.com/wordpress/?p=10204 鑫空间-鑫生活
本文欢迎分享,欢迎转发,欢迎聚合,全文转载请联系授权。
关于 visibilitychange 事件,在 2012 年的时候我就撰文介绍过了,详见“Page Visibility(页面可见性) API介绍、微拓展”,是一个非常有历史的 Web 特性,不过实际开发中,由于浏览器实现的细节差异,Page Visibility API 并不总是能够满足实际的生成需求,这是怎么回事呢?
本文就会从 visibilitychange 事件触发,带大家深入了解下 Web 页面的生命周期过程。
一、Safari下问题说明
在 Safari 浏览器下,无论是桌面端 Safari,还是 iOS Safari,visibilitychange 事件不总是触发的。
对于窗口最小化,Tab 隐藏等行为 visibilitychange 事件是正常的,但是如果是点击页面某个链接发生的当前页导航跳转,则 visibilitychange 事件不会触发。
所以,虽然 visibilitychange 看起来兼容性不错,IE10+支持,但是实际使用的时候还是有一些问题的,上述问题在 caniuse 上也是有对应的描述的。
这就会给我们的业务开发带来困扰,例如,有一个数据上报的需求,希望用户不再访问此页面的时候,进行一次数据上报,则如果使用 visibilitychange 事件进行处理,Safari 浏览器下就会有数据异常的情况发生。
document.addEventListener('visibilitychange', function logData() { if (document.visibilityState === 'hidden') { navigator.sendBeacon('/log', { /* 要发送的数据 */ }); } });
那有没有什么办法解决这个问题呢?
那就是使用 pagehide 事件。
二、和pageshow/pagehide的区别
1. 功能区别
虽然都是有显示与隐藏的含义,但是 visibilitychange 指的是页面的可见与不可见,pageshow/pagehide 指的是页面的进入与离开。
我们可以通过下面一段测试代码了解两者功能上的区别:
<div id="result"></div>
log = function (content) { result.innerHTML += content + '<br>'; }; window.addEventListener('pageshow', function () { log('pageshow: 页面显示'); }); window.addEventListener('pagehide', function () { log('pagehide: 页面隐藏'); }); document.addEventListener('visibilitychange', function () { if (document.hidden) { log('visibilitychange: 页面隐藏'); } else { log('visibilitychange: 页面显示'); } });
具体描述为:
- 页面进入,包括刷新会触发 pageshow;
- 选项卡切换,只会触发 visibilitychange 显示与隐藏;
- 前进和后退,所有浏览器都会依次触发 pagehide,visibilitychange 和 pageshow;
- 如果是点击某个链接跳转出去,则Safari浏览器会出现不一样的表现。
大家若有兴趣,可以访问这里感受下事件变化的触发。其实上面的第 4 点大家可以在 Safari 浏览器下测试下,点击页面链接然后再返回,会发现 visibilitychange 事件并未执行。
2. 用法区别
'visibilitychange'
事件通常都是挂载在 document 对象上,虽然现在最新的浏览器也支持挂载在 window 对象上,不过由于 Safari 14 之前的版本不支持,因此,是不推荐使用下面的语法的:
window.addEventListener('visibilitychange', () => {});
而 pageshow 和 pagehide 事件都是通过 window 对象进行注册的。
3. 兼容性区别
pageshow 和 pagehide 事件是 IE11 及其以上浏览器支持的,而 visibilitychange 事件是 IE10 及其以上版本支持的。
具体如下截图示意:
虽然 pageshow 和 pagehide 的兼容性略逊一筹,但是人家稳定啊,以及放眼整个世界,使用 IE10 浏览器的用户微乎其微,因为 IE10 就是个过渡版本。
三、unload 和 beforeunload 事件呢?
除非是要兼容古老的 IE 浏览器,以及在桌面端浏览器环境下阻止用户退出网页(如,您写的内容尚未保存,是否退出,如下代码所示),否则,没有任何理由使用 unload 和 beforeunload 事件,尤其是移动端的页面。
window.addEventListener('beforeunload', function (event) { if (pageHasUnsavedChanges()) { event.preventDefault(); return event.returnValue = '您写的内容尚未保存,是否退出?'; } });
因为用户访问完一个页面,往往是直接切换到其他 APP,然后通过杀进程关掉整个浏览器 APP,unload 事件就不会触发。
以及另外一个比较重要的原因,unload 和 beforeunload 会阻止浏览器把页面存入缓存,影响浏览器前进和后退时候的响应速度。
四、痛快点,终极方案是什么?
回到一开始,只是说了 pagehide 解决 Safari 的问题,可具体该如何解决呢?
很简单,判断是不是 Safari 浏览器,然后额外增加一个 pagehide 事件:
document.addEventListener('visibilitychange', function logData() { if (document.visibilityState === 'hidden') { navigator.sendBeacon('/log', postData ); } }); if (/^((?!chrome|android).)*safari/i.test(navigator.userAgent)) { window.addEventListener('pagehide', function () { navigator.sendBeacon('/log', postData ); }); }
但是,上面的实现其实是有风险的,因为你并不知道哪一天 Safari 浏览器会改变自己的策略,也就是说不定 Safari 16 或者后面某一个版本 pagehide 也会触发 visibilitychange 行为,则上面的代码又会有重复上报的问题。
所以,比较稳妥,且自己不需要动脑子的方法,就是拾人牙慧,使用他人已经做好的项目进行开发,例如谷歌实验室开源的这个名为 PageLifecycle.js 的项目:github.com/GoogleChromeLabs/page-lifecycle
使用如下:
<script src="./lifecycle.es5.js"></script> <script> lifecycle.addEventListener('statechange', function(event) { console.log('状态变化:' + event.oldState + ' → ' + event.newState); }); </script>
此时,当我们导航跳转再返回,就会出现如下截图所示的输出效果:
您也可以访问这里亲自感受下输出结果。
Safari 下虽然细节上有差异,但是从 passive → hidden 这个状态和 Chrome 浏览器是一致的,如下截图所示:
所以,我们希望页面离开时候上报数据,可以试试下面的代码,理论上应该是没问题的:
lifecycle.addEventListener('statechange', function(event) { if (event.oldState == 'passive' && event.newState == 'hidden') { navigator.sendBeacon('/log', postData); } });
上述截图除了 passive 和 hidden 这了两个状态,还出现了 active 和 frozen,这些状态都表示什么意思呢,是浏览器原本就有的,还是 PageLifecycle.js 自定义的呢?
都是浏览器都有的,写入规范标准的状态,都属于页面生命周期的一部分。
//zxx: 如果你看到这段文字,说明你现在访问是不是原文站点,更好的阅读体验在这里:https://www.zhangxinxu.com/wordpress/?p=10204(作者张鑫旭)
五、了解页面的生命周期
完整的页面生命周期状态包括这些:
- ACTIVE 激活
- PASSIVE 未激活(页面可以看到,但焦点不在此页面,打开开发者工具可以触发此状态)
- HIDDEN 隐藏,最小化、标签页切换都属于隐藏
- FROZEN 冻结
- TERMINATED 结束 (页面被关闭)
- DISCARDED 废弃(页面内容被浏览器清空)
其中,从 HIDDEN 状态到 FROZEN 状态之间的变化是有新的 API 事件名称检测的,分别是 resume 事件和 freeze 事件,使用示意如下:
document.addEventListener('freeze', (event) => { // 页面被冻结 }); document.addEventListener('resume', (event) => { // 页面解冻了 });
Web 网页完整的生命周期流程见下面的高清大图(看不清可双指放大,或点击小图查看),原图是英文的,源自 google 官方的这篇文章,自己重新翻译了下,方便大家的学习。
DISCARDED 废弃
其中,废弃状态是后来才有的,原本是没有的,目的是为了释放不必要的内存开销。
如果经常使用 Chrome 浏览器,应该都有遇到过这样的现象,就是一个很久没有访问的标签页再切换过去的时候,页面会重新加载一遍。
之所以会加载,是因为浏览器为了节约内存,把这个长时间不使用的页面给废弃了,所有页面的内存、缓存通通舍弃。
我个人是不太喜欢这样的处理的,因为有些页面,特别是图特别多的大型的文档(如 figma 设计稿),每次切换过去,都要重新 loading 一次,很不爽的。
关于这个,可以所啰嗦两句。
原本 IE 时代,Chrome 还没出现的时候,浏览器的标签页,如果你开了多个,只要 1 个崩掉了,整个浏览器都会崩溃,其他的标签页数据就会丢失。
当然 Chrome 出来的时候,其中宣传的一个优点就是每个标签页面独立,A 页面崩溃不会影响 B 页面,但是,这种不崩溃策略是以牺牲内存为代价的,因此,那个时候,经常有网络图戏谑 Chrome 是个内存怪兽(例如下面这个 GIF 图 – 1.05M 点击播放)。
而现在的这种冻结+废弃的策略,虽然省了内存,但是牺牲了用户体验,正所谓鱼和熊掌不可兼得,所以终极解决方法还是加大内存,16G内存走起。
在 Chrome 68 之后,我们可以使用 document.wasDiscarded
判断页面是不是处于废止状态。
以及,也可以在 Chrome 浏览器地址栏中输入 chrome://discards 查看各个页面的状态。
例如,我现在看了下(省略中间十几个大同小异):
可以看到,除了几个新打开不久的页面,其他页面都已经 DISCARDED 掉了,惨!
不说了,我要去找运维申请加内存条了。
六、例行的结尾内容
好,以上就是本文的全部内容,如果文中有表述不准确的地方,欢迎指正。
风萧萧兮易水寒,写作头发兮不复还,这句诗充分表现了写作的不易与辛苦,连头发都掉没了,所以,求分享,求转发。
参考文章
本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
本文地址:https://www.zhangxinxu.com/wordpress/?p=10204
(本篇完)
- JS CustomEvent自定义事件传参小技巧 (0.236)
- 借助HTML ping属性实现数据上报 (0.236)
- CSS按钮(a/button)生命周期的一些认识 (0.157)
- 与web网页设计师内部交流会内容预分享 (0.157)
- Page Visibility(页面可见性) API介绍、微拓展 (0.157)
- 今天才知道,Web网页也能阻止息屏了 (0.157)
- URL锚点HTML定位技术机制、应用与问题 (0.118)
- 小tips: 页面链接跳转历史URL不记录的兼容处理 (0.118)
- JS URL()和URLSearchParams() API接口详细介绍 (0.118)
- ajax与HTML5 history pushState/replaceState实例 (0.094)
- JS获取上一访问页面URL地址document.referrer实践 (RANDOM - 0.094)
大佬,我发现一个问题
visibilitychange 事件,在我当前页面打开控制台,来回切换Tab,这个时候visibilitychange 事件就不会生效,只有关闭控制台关闭才生效,这是为什么呢?
可惜lifecycle在微前端里子iframe里没发使用
visibilitychange 在大多场景是适配的,但是在ios,app下如果进行强制关闭进程会存在问题
实测 safari 14.1 pagehide 触发的时候,同时会触发visibilitychange, 页面隐藏。
链接离开再返回
pageshow: 页面显示
pagehide: 页面隐藏
visibilitychange: 页面隐藏
visibilitychange: 页面显示
pageshow: 页面显示
另外:如果 chrome 开了太多 tab,导航离开之后返回时只会触发 pageshow 事件。
alert(1223)
前进和后退,所有浏览器都会依次触发 pagehide,visibilitychange 和 pageshow;
谷歌浏览器测试下来,好像前进后退不会触发visibilitychange 和 pageshow;
一如既往的高质量文章,支持张哥
从js visibilitychange Safari下无效说开去
这个应用场景有哪些吗?我能想到的比如监控用户停留的页面时长,或者可以配合在线考试,判断用户是否离开了考试页面。
转发了