event.persisted === true 是页面状态需抢救的唯一可信信号;它在 pagehide 中标识 bfcache 或冻结,pageshow 中需校验该值及 localStorage 存在性才恢复,且仅同步可序列化字段。
移动端浏览器(尤其是 iOS Safari)在页面退到后台时,pagehide 会照常触发,但关键不是它“发生了”,而是 event.persisted 的值。只有当这个布尔值为 true 时,才表示浏览器打算保留页面状态——大概率走 bfcache 或冻结路径,而不是直接卸载。
常见错误现象:只监听 pagehide 就调用保存逻辑,结果在 Chrome Android 上重复触发(比如切到其他 App 又快速切回),或在 Safari 上漏掉冻结前的最后机会。
event.persisted === false 说明页面即将销毁(如用户点关闭标签),此时保存无意义,甚至可能污染 localStorageevent.persisted === true 才是真正需要抢救状态的时刻;但要注意,它不保证后续一定能 resume,只是“尽力而为”pagehide 回调里读取 element.scrollHeight 或 getBoundingClientRect() —— 页面已不可见,这些值常返回 0
pageshow 确实会在页面从冻结或 bfcache 恢复时触发,但它也会在普通刷新后触发(此时 event.persisted === false)。如果你不加判断就执行恢复,用户刚刷新完表单,瞬间又被 localStorage 里的旧草稿覆盖。
正确做法是双条件检查:
event.persisted === true
localStorage.getItem('uiState') 不为 null)localStorage.removeItem('uiState')),避免下次 pageshow 再次误恢复freeze 事件理论上更贴近“冻结”动作本身,但它有严重兼容短板:iOS Safari 直到 16.4+ 才稳定支持,旧版本静默忽略;部分 Android WebView 根本不派发该事件。所以不能单靠它。
实操上必须组合使用:
freeze 回调,在里面调用 saveState() 并设 frozen = true
pagehide,仅当 event.persisted && !frozen 时补存一次performance.now() 记录上次保存时间,200ms 内不再写入,防止两个事件紧挨着触发导致冗余从冻结中 resume 或 pageshow 恢复时,DOM 和事件监听器都还在,只是 JS 执行暂停了。误以为要“重新初始化”会导致定时器重复启动、请求重复发送、UI 状态冲突。
真正该做的只有三件事:
window.scrollTo(0, saved.scrollY),别碰 element.scrollTop(元素可能已被重建)input.value、textarea.value、select.selectedIndex 这类原始属性video.currentTime = saved.time,前提是保存时没存 DOM 节点或函数引用容易被忽略的是:保存内容必须纯 JSON 可序列化。存了 document.body 或 setTimeout 回调,JSON.stringify() 会静默丢掉字段,恢复时根本拿不到。