JS44 页面生命周期
W3C指定了Page Lifecycle API,统一了网页从诞生到卸载的行为模式,并且定义了新的事件,允许开发者响应网页状态的各种转换。
新的API
生命周期
网页的生命周期被分为了六个阶段,每个时刻只可能处于其中一个阶段。
(1)Active 阶段
网页处于可见状态,且拥有输入焦点。
(2)Passive阶段
此阶段网页可见,但是没有输入焦点,无法接受输入。UI更新(比如动画)仍然在执行。
(3)Hidden阶段
此阶段用户的桌面被其他窗口占据,网页不可见,当尚未冻结。UI更新不再执行。
(4)Terminated阶段
由于用户主动关闭窗口或者在同一个窗口但前往其他页面,导致当前页面被浏览器卸载并从内存中清除,页面进入此阶段。
这个阶段总是在Hidden阶段之后发生,此阶段不会自动任何新任务,正在进行的任务可能会被终止。
(5)Frozen阶段
页面处于Hidden阶段时间过长,但网页一直未关闭,页面会被冻结,进入Frozen阶段。也有可能处于可见状态的页面长时间没有操作,也会进入Frozen阶段。
这个阶段,网页不会再被分配CPU资源,定时器、回调函数、网络请求、DOM操作都不会执行,但是正在执行的任务会被执行完。浏览器可能会允许Frozen阶段的页面,周期性复苏一小段时间,短暂变回Hidden阶段,允许一小部分任务执行。
(6)Discarded阶段
网页长时间处于Frozen阶段,用户未唤醒页面,就会进入Discarded阶段。此阶段浏览器自动卸载网页,清楚网页内存占用。Passive阶段的网页如果长时间没有互动,也可能直接进入Discarded阶段。
这个阶段一般是系统强制执行,任何新任务和JavaScript代码都不能执行。
网页Discarded周,Tab窗口还在,如果用户重新访问Tab,浏览器会重新向服务器发出请求,再次加载网页,回到Active阶段
常见场景
(1)用户打开网页,切换到其他App,过了一会又重新回到网页
1 | Active → Hidden → Active |
(2)用户打开网页,切换到其他App,并且长时间使用后者,导致系统自动丢弃网页
1 | Active → Hidden → Frozen → Discarded |
(3)用户打开网页,切换到其他App,然后在任务管理器中将浏览器进程清楚。
1 | Active → Hidden → Terminated |
(4)系统丢弃了某个Tab里面的页面后,用户重新打开这个Tab
1 | Discarded → Active |
事件
网页的生命周期是在所有帧触发,不管是底层的帧,还是内嵌的帧。也就是说,内嵌的<iframe>
网页跟顶层的网页一样,都会同时监听到所有的生命周期事件。
focus事件
focus
事件在页面获得输入焦点时触发,比如网页从Passive阶段变为Active阶段。
1 | window.addEventListener('focus', () => console.log('focus')) |
blur事件
blur
事件在页面失去输入焦点时触发,比如页面从Active阶段变为Passive阶段。
1 | window.addEventListener('blur', (e) => console.log('blur')) |
visibilitychange事件
visibilitychange
事件在网页可见状态发生变化时触发,一般发生在下面的场景:
- 用户隐藏页面(切换Tab,最小化浏览器),页面由Active阶段变为Hidden阶段
- 用户重新访问隐藏的页面,页面由Hidden阶段进入Active阶段
- 用户关闭页面,页面先进入Hidden阶段,然后进入Terminated阶段。
Page Visibility API在document
对象上新增了document.visibilityState
属性,该属性返回一个字符串,表示页面当前的可见性状态,有三个可能的值:
hidden
,页面彻底不可见visibile
,页面至少一部分课件prerender
,页面即将或正在渲染,处于不可见状态
通过监听visibilitychange
事件,判断document.visibilityState
属性,就可以获得页面的可见状态。
1 | window.addEventListener('visibilitychange', (e) => console.log(document.visibilityState)); |
由于历史兼容性的原因,document
上定义了hidden
属性,这是一个只读的属性,返回一个布尔值,表示当前页面是否可见。应当尽量避免使用这个属性。
freeze
freeze
事件在网页进入Frozen阶段时触发,这个事件的监听函数,最长只能运行500毫秒,并且只能复用已经打开的网络连接,不能发起新的网络请求
1 | document.addEventListener('freeze', (e) => console.log('freeze')) |
注意,从Frozen阶段进入Discarded阶段,不会触发任何事件,无法指定回调函数,只能在进入Frozen阶段时指定。
resume事件
resume
事件在网页离开Frozen阶段,变为Active/Passive/Hidden阶段时触发
1 | document.addEventListener('resume', (e) => console.log('resume')) |
pageshow事件
pageshow
事件在用户加载网页时触发,有可能是全新的页面加载,也有可能是从缓存中获取的页面。如果是从缓存中获取的页面,则该事件对象的event.persisted
为true
,否则为false
1 | window.addEventListener('pageshow', () => console.log('pageshow')); |
这个事件与页面的可见性无关,至于浏览器的History记录变化有关。
pagehide事件
pagehide
事件在用户离开网页、进入另一个网页时触发。它同样与页面的可见性没有关系,至于浏览器的History记录有关系。
1 | window.addEventListener('pagehide', () => console.log('pagehide')); |
如果浏览器能够将当前页面添加到缓存以供稍后重用,那么事件对象的event.persisted
属性为true
,否则为false
。如果为true
时,页面添加到了缓存,则页面进入Frozen阶段,否则进入Terminated状态。
beforeunload事件
beforeunload
事件在窗口或文档即将卸载时触发,改时间发生时,文档依然可以见,此时卸载仍可取消。经过这个时间,网页进入Terminated状态。
1 | window.addEventListener('beforeunload', function (event) { |
当为event.returnValue
显式的设置任何值时,弹出确认窗口让用户自行选择是否关闭当前页面。没有赋值时,该事件不做任何响应。
从2011年5月25日起, HTML5规范声明:在该事件的处理函数中调用下列弹窗相关的方法时,可以忽略不执行,window.showModalDialog()
, window.alert()
, window.confirm()
和window.prompt()
要注意的是,beforeunload
事件只应该用于提醒用户尚未保存的更改,如果这些更改已经保存,那么这个事件应该被移除。这个事件不应该无条件的加载到页面,因为这样做在某些情况下会造成性能的问题。
unload事件
unload
事件在页面正在卸载时触发,经过这个事件,页面进入Terminated状态。
注意,不建议使用unload
事件,因为这个时间是不可靠的,并且可能在某些情况下会有性能问题。
获取页面状态
如果页面处于Active、Passive、Hidden阶段,可以通过下面代码获取页面状态:
1 | function getState() { |
如果页面处于Frozen和Terminated阶段,由于定时器代码不会执行,只能通过事件监听判断状态,进入Frozen阶段,可以监听freeze
时间,进入Terminated
阶段,可以监听pageHide
阶段
如果某个选项卡处于Frozen阶段,就随时有可能被系统丢弃,进入Terminated阶段。如果用户再次点击该选项卡,浏览器会重新加载该页面。可以通过判断document.wasDicarded
属性,为true
证明网页曾经被丢弃过。
如果页面被丢弃过,window
对象上会新增window.clientId
和window.discardedClientId
两个属性,用来恢复丢弃前的状态
DOMContentLoaded
事件和load
事件
在页面开始加载时,会先后触发这两个事件。可以通过监听DOMContentLoaded
事件和load
事件来衡量页面的加载情况。
DOMContentLoaded
事件表示DOM树构建完毕,可以安全的访问DOM树所有节点、绑定事件等(不需要等待图片、音视频等资源下载完成),jQuery的ready
事件监听的就是这个事件)
而load
事件表示所有资源都已经加载完毕,图片、背景内容都完成渲染,页面出于可交互状态,jQuery的load
事件监听的就是这个事件