我该如何监听localStorage和sessionStorage?
尝试监听页面的localStorage时发现同页无法触发绑定的事件,记录一下问题原因、解决方案以及几个容易忽略的知识点。
遇到的问题
做博客暗黑模式切换时,我把主题状态存在了 localStorage 里。最开始很自然地想到直接监听 storage 事件,代码大概像下面这样:
window.addEventListener('storage', () => {
// 一些操作
});结果测试时发现有点怪。打开两个 tab 页,用 localStorage.setItem() 修改值后,当前页面完全没有触发监听,另一个同源页面的控制台倒是能收到事件。
再换成 sessionStorage.setItem() 试了一下,当前页同样不触发,多 tab 场景下也基本没反应。
先说结论,免得后面绕晕:
storage事件不会在”发起修改的当前页面”触发。localStorage跨 tab 好使,sessionStorage不行。
为什么监听不到
1. storage 事件压根不是给你自己用的
window.addEventListener('storage') 监听到的不是”我调用了 setItem”,而是”另一个同源文档的 storage 变了”。
- 当前页面调
localStorage.setItem(),自己收不到事件。 - 其它同源页面能收到。
浏览器的逻辑大概是:你自己改的值,你自己肯定知道,没必要再通知你一遍。
2. localStorage 和 sessionStorage 共享范围不一样
localStorage 同源下长期保存,不同 tab、不同窗口都能共享,天然适合做多页面状态同步。
sessionStorage 生命周期跟当前会话页绑定,默认不会在多个 tab 间共享。所以普通多 tab 测试场景下基本看不到同步效果,storage 事件在它身上也就更”像没生效”。
storage 事件什么时候触发
触发条件和直觉不太一样,分开看:
localStorage:A 页面执行 setItem,A 自己不触发,同源的 B、C 页面能收到。
sessionStorage:A 页面执行 setItem,A 自己不触发,新开的 B tab 通常也收不到——因为 B 和 A 不共享 sessionStorage。
所以如果需求是”同页面里某个组件改了 storage,另一个组件要立刻响应”,或者”当前页面自己也要拿到变化通知”,原生 storage 事件不够用。
解决方案
浏览器不给当前页面派发 storage 事件,那就自己补一个。思路就是在调用 setItem、removeItem、clear 时,手动派发一个自定义事件,把变更信息带出去。
最简单的做法
最开始我就重写了 setItem,能用就行:
const originalSetItem = window.localStorage.setItem;
window.localStorage.setItem = function (key, newValue) {
const eventTemp = new Event('setThemeColor');
window.dispatchEvent(eventTemp);
originalSetItem.apply(this, [key, newValue]);
};
window.addEventListener('setThemeColor', handleFunc);不过这个方案比较粗糙,removeItem 和 clear 没管,也不知道是哪个 key 变了。
更完整一点的封装
上面那个方案还有几个问题:removeItem、clear 没处理,也不知道哪个 key 变了、前后值是什么。
改进一下,派发带 detail 的 CustomEvent:
type StorageType = 'localStorage' | 'sessionStorage';
interface WebStorageChangeDetail {
type: StorageType;
key: string | null;
oldValue: string | null;
newValue: string | null;
}
const WEB_STORAGE_CHANGE_EVENT = 'web-storage-change';
function dispatchStorageChange(detail: WebStorageChangeDetail) {
window.dispatchEvent(
new CustomEvent<WebStorageChangeDetail>(WEB_STORAGE_CHANGE_EVENT, {
detail,
})
);
}
function patchStorage(storage: Storage, type: StorageType) {
const originalSetItem = storage.setItem;
const originalRemoveItem = storage.removeItem;
const originalClear = storage.clear;
storage.setItem = function (key, value) {
const oldValue = storage.getItem(key);
originalSetItem.call(this, key, value);
dispatchStorageChange({
type,
key,
oldValue,
newValue: value,
});
};
storage.removeItem = function (key) {
const oldValue = storage.getItem(key);
originalRemoveItem.call(this, key);
dispatchStorageChange({
type,
key,
oldValue,
newValue: null,
});
};
storage.clear = function () {
originalClear.call(this);
dispatchStorageChange({
type,
key: null,
oldValue: null,
newValue: null,
});
};
}
patchStorage(window.localStorage, 'localStorage');
patchStorage(window.sessionStorage, 'sessionStorage');监听时就能拿到完整的变更信息:
window.addEventListener(WEB_STORAGE_CHANGE_EVENT, (event) => {
const { detail } = event as CustomEvent<WebStorageChangeDetail>;
console.log('storage 类型:', detail.type);
console.log('变化的 key:', detail.key);
console.log('旧值:', detail.oldValue);
console.log('新值:', detail.newValue);
});主题切换、登录态缓存、表单草稿,都能用这一套。
跨 tab 监听怎么办
实际项目里,两种监听往往要同时存在:
window.addEventListener('storage', (event) => {
console.log('其它页面触发的 storage 变化:', event.key, event.newValue);
});
window.addEventListener(WEB_STORAGE_CHANGE_EVENT, (event) => {
const { detail } = event as CustomEvent<WebStorageChangeDetail>;
console.log('当前页面触发的 storage 变化:', detail.key, detail.newValue);
});一个管”别的页面改了什么”,一个管”我自己改了什么”,各管各的。
几个容易忽略的点
storage 事件里能拿到什么
原生 StorageEvent 有 key、oldValue、newValue、storageArea(localStorage 还是 sessionStorage)、url(触发修改的文档地址)。如果是 clear(),前三个通常是 null。
setItem 只认字符串
传数字、布尔、对象进去都会变字符串。对象要自己 JSON.stringify(),取值再 JSON.parse():
localStorage.setItem('user', JSON.stringify({ name: 'haimin' }));
const user = JSON.parse(localStorage.getItem('user') || '{}');监听到值变化后要处理的话,别忘了先处理格式。
写入相同值不会触发变化
同一个 key 反复设置成一样的值,storage 实际没有变化,事件也不会触发。调试时如果发现事件没按预期来,先检查一下是不是写了重复内容。
patch 只做一次
如果把重写逻辑放在 React 组件里,组件重复挂载就会出现方法被重复重写、事件重复派发的问题。最好在应用初始化阶段 patch 一次,或者干脆封装一层统一的 storage 工具函数,所有读写都走自己的工具,别到处直接 localStorage.setItem()。
我博客里暗黑模式同步评论区主题,用的就是这个思路。
更复杂的场景可以看 BroadcastChannel
localStorage + storage 本质上是”顺便能做同步”,不是专门给实时通信用的。如果需求比较重——多页面同步登录态、实时通知 tab 刷新、共享操作队列——BroadcastChannel 更直接。轻量状态同步的话,localStorage + 自定义事件 够用了。
我的实际用法
博客暗黑模式切换就是个典型场景:主题存在 localStorage,切换时要让页面和评论区一起更新,原生 storage 事件不够用。
做法就是:切换主题 → 写入 localStorage → 派发自定义事件 → 依赖主题的组件监听并同步。方案不高级,但够用。
小结
- 监听其它同源页面的变化 → 原生
storage事件 - 监听当前页面自己的变化 → 自定义事件
- 要兼顾
removeItem、clear和完整上下文 → 做一次统一封装
一开始就是想解决”为什么监听不到”的问题,搞明白之后发现,API 不难,触发边界才是坑。