我该如何监听localStorage和sessionStorage?

尝试监听页面的localStorage时发现同页无法触发绑定的事件,记录一下问题原因、解决方案以及几个容易忽略的知识点。

作者 haiminovo#开发#前端#WebStorage

遇到的问题

做博客暗黑模式切换时,我把主题状态存在了 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. localStoragesessionStorage 共享范围不一样

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 事件,那就自己补一个。思路就是在调用 setItemremoveItemclear 时,手动派发一个自定义事件,把变更信息带出去。

最简单的做法

最开始我就重写了 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);

不过这个方案比较粗糙,removeItemclear 没管,也不知道是哪个 key 变了。

更完整一点的封装

上面那个方案还有几个问题:removeItemclear 没处理,也不知道哪个 key 变了、前后值是什么。

改进一下,派发带 detailCustomEvent

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 事件里能拿到什么

原生 StorageEventkeyoldValuenewValuestorageArealocalStorage 还是 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 事件
  • 监听当前页面自己的变化 → 自定义事件
  • 要兼顾 removeItemclear 和完整上下文 → 做一次统一封装

一开始就是想解决”为什么监听不到”的问题,搞明白之后发现,API 不难,触发边界才是坑。

允许规范转载