惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
云风的 BLOG
云风的 BLOG
Apple Machine Learning Research
Apple Machine Learning Research
Hugging Face - Blog
Hugging Face - Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Google DeepMind News
Google DeepMind News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
B
Blog
月光博客
月光博客
博客园 - 【当耐特】
WordPress大学
WordPress大学
Microsoft Azure Blog
Microsoft Azure Blog
I
InfoQ
The GitHub Blog
The GitHub Blog
Engineering at Meta
Engineering at Meta
Jina AI
Jina AI
博客园 - Franky
MyScale Blog
MyScale Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Last Week in AI
Last Week in AI
B
Blog RSS Feed
H
Help Net Security

张鑫旭-鑫空间-鑫生活

1分钟速度了解text-align match-parent声明 « 张鑫旭-鑫空间-鑫生活 光标的形状也能设置了,就是CSS caret-shape属性 « 张鑫旭-鑫空间-鑫生活 pretext与文字四面环绕效果的实现 « 张鑫旭-鑫空间-鑫生活 JSON.rawJSON方法的作用是什么? « 张鑫旭-鑫空间-鑫生活 如何使用CSS判断鼠标从哪个方向进入元素? « 张鑫旭-鑫空间-鑫生活 借助mediabunny纯JS实现视频水印、剪裁、合成等功能 « 张鑫旭-鑫空间-鑫生活 务必谨慎使用JS WeakRef弱引用 « 张鑫旭-鑫空间-鑫生活 CSS六边形头像的实现与蜂巢布局 « 张鑫旭-鑫空间-鑫生活 CSS corner-shape与背景底纹技术 « 张鑫旭-鑫空间-鑫生活 浅学WebTransport API:下一代Web双向通信技术 « 张鑫旭-鑫空间-鑫生活 HTML dialog元素新支持了closedBy属性 « 张鑫旭-鑫空间-鑫生活 HTML interestfor属性与悬停popover交互效果 « 张鑫旭-鑫空间-鑫生活 CSS3模拟window7炫酷界面效果展示 « 张鑫旭-鑫空间-鑫生活 图片旋转+剪裁js插件(兼容各浏览器) « 张鑫旭-鑫空间-鑫生活 纯js无图片无外链CSS图片幻灯片播放插件 « 张鑫旭-鑫空间-鑫生活 CSS border三角、圆角图形生成技术简介 « 张鑫旭-鑫空间-鑫生活 CSS “渐进增强”在web制作中常见应用举例 « 张鑫旭-鑫空间-鑫生活 让IE6/IE7/IE8浏览器支持CSS3属性 « 张鑫旭-鑫空间-鑫生活 jQuery – 高亮动态显示页面HTML代码插件 « 张鑫旭-鑫空间-鑫生活 翻译 – 逐渐消失的Flash网站 « 张鑫旭-鑫空间-鑫生活 CSS reset的重新审视 – 避免样式重置 « 张鑫旭-鑫空间-鑫生活 新浪微博插入话题后部分文字选中的js实现 « 张鑫旭-鑫空间-鑫生活 JavaScript实现http地址自动检测并添加URL链接 « 张鑫旭-鑫空间-鑫生活 CSS实现兼容性的渐变背景(gradient)效果 « 张鑫旭-鑫空间-鑫生活 CSS content内容生成技术以及应用 « 张鑫旭-鑫空间-鑫生活 CSS gradient渐变之webkit核心浏览器下的使用 « 张鑫旭-鑫空间-鑫生活 CSS渐变之CSS3 gradient在Firefox3.6下的使用 « 张鑫旭-鑫空间-鑫生活 区分IE8/IE7/IE6及其他浏览器-CSS “\9″hack « 张鑫旭-鑫空间-鑫生活 CSS实现跨浏览器兼容性的盒阴影效果 « 张鑫旭-鑫空间-鑫生活 CSS3&HTML5各浏览器支持情况一览表 « 张鑫旭-鑫空间-鑫生活
深度好文: 从js visibilitychange Safari下无效说开去 « 张...
张鑫旭,zhangxinxu · 2021-11-24 · via 张鑫旭-鑫空间-鑫生活

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

这就会给我们的业务开发带来困扰,例如,有一个数据上报的需求,希望用户不再访问此页面的时候,进行一次数据上报,则如果使用 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 事件并未执行。

safari visibilitychange未执行示意图

2. 用法区别

'visibilitychange' 事件通常都是挂载在 document 对象上,虽然现在最新的浏览器也支持挂载在 window 对象上,不过由于 Safari 14 之前的版本不支持,因此,是不推荐使用下面的语法的:

window.addEventListener('visibilitychange', () => {});

而 pageshow 和 pagehide 事件都是通过 window 对象进行注册的。

3. 兼容性区别

pageshow 和 pagehide 事件是 IE11 及其以上浏览器支持的,而 visibilitychange 事件是 IE10 及其以上版本支持的。

具体如下截图示意:

pageshow兼容性

虽然 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 会阻止浏览器把页面存入缓存,影响浏览器前进和后退时候的响应速度。

unload 事件兼容性

四、痛快点,终极方案是什么?

回到一开始,只是说了 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>

此时,当我们导航跳转再返回,就会出现如下截图所示的输出效果:

lifecycle.js 示意

您也可以访问这里亲自感受下输出结果。

Safari 下虽然细节上有差异,但是从 passive → hidden 这个状态和 Chrome 浏览器是一致的,如下截图所示:

Safari 下的生命周期状态变化

所以,我们希望页面离开时候上报数据,可以试试下面的代码,理论上应该是没问题的:

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 点击播放)。

Chrome 内存怪兽

而现在的这种冻结+废弃的策略,虽然省了内存,但是牺牲了用户体验,正所谓鱼和熊掌不可兼得,所以终极解决方法还是加大内存,16G内存走起。

在 Chrome 68 之后,我们可以使用 document.wasDiscarded 判断页面是不是处于废止状态。

以及,也可以在 Chrome 浏览器地址栏中输入 chrome://discards 查看各个页面的状态。

例如,我现在看了下(省略中间十几个大同小异):

标签页状态查看

可以看到,除了几个新打开不久的页面,其他页面都已经 DISCARDED 掉了,惨!

不说了,我要去找运维申请加内存条了。

六、例行的结尾内容

好,以上就是本文的全部内容,如果文中有表述不准确的地方,欢迎指正。

风萧萧兮易水寒,写作头发兮不复还,这句诗充分表现了写作的不易与辛苦,连头发都掉没了,所以,求分享,求转发。

参考文章

本文为原创文章,欢迎分享,勿全文转载,如果实在喜欢,可收藏,永不过期,且会及时更新知识点及修正错误,阅读体验也更好。
本文地址:https://www.zhangxinxu.com/wordpress/?p=10204

(本篇完)