实现可过期的 localstorage 数据
localStorage
默认不会过期,数据会一直保留,除非用户手动清除。因此要实现 具有过期机制的 localStorage 缓存
主要有两种解决办法:
- 惰性删除
- 定时删除
1.惰性删除
1.含义:
只有在读取某个键值时才判断其是否过期,若过期则删除该项并返回空值
2.实现思路
存储数据时,将数据和过期时间一起存入(如使用一个对象 { value, expire }
)。
获取数据时,检查 expire
是否早于当前时间,若过期则删除该项。
3.优缺点
优点:实现较为简单,不需要额外的定时器
缺点:如果莫一条数据一直没有被读取到,那么可能就不会被删除,会占用内存
4.代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| function setItem(key, value, expires) { const data = { value: value, expires: Date.now() + expires * 1000, }; localStorage.setItem(key, JSON.stringify(data)); }
function getItem(key) { const data = JSON.parse(localStorage.getItem(key)); if (!data) { return null; } if (data.expires < Date.now()) { localStorage.removeItem(key); return null; } return data.value; }
|
2.定时删除
1.含义:
通过定时任务周期性扫描所有存储项,删除过期的数据。
2.实现思路
维护所有带过期时间的数据的 key 列表。
使用 setInterval()
每隔一定时间检查这些 key 是否已过期。
每次最多清理固定数量(如 5 个),以防阻塞主线程。
3.优缺点
优点:可以主动释放空间,避免惰性删除留下的冗余数据。
缺点:需要占用一定的系统资源。
4.代码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| function startCleanner(interval = 2000, linmitCount = 5) { setInterval(() => { let keytoRemove = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); const item = JSON.parse(localStorage.getItem(key)); if (item?.expire && Date.now() > item.expire) { keytoRemove.push(key); } if (keytoRemove.length >= linmitCount) { break; } } keytoRemove.forEach((key) => { localStorage.removeItem(key); }); }, interval); }
|
问题:
为什么每次最多清除固定数量的 5 个?
原因是:JavaScript 是单线程的,这样操作为了防止防阻塞主线程
JavaScript 在浏览器中运行在主线程上,这条主线程负责:
- 执行 JavaScript 代码;
- 处理用户交互(点击、滚动等);
- 页面渲染(布局、绘制等);
- 网络请求回调等。
如果我们在主线程上执行了耗时的操作,比如:
- 遍历大量的
localStorage
数据; - 进行 JSON 解析(
JSON.parse
); - 判断过期、删除数据(
localStorage.removeItem
);
那么整个主线程就会“卡住”,UI 渲染和用户响应都会变慢或暂时停止,用户会感觉页面卡顿或无响应。
其次:
localStorage
操作是同步的
localStorage.getItem()
、setItem()
、removeItem()
等方法都是同步阻塞式的。
为什么限制“每次删除 5 个”可以缓解阻塞?
每次只处理少量数据(例如 5 个),每轮清理的耗时可控(比如 < 16ms,即 1 帧时间),不会卡顿。
localStorage
是同步 API;
大量同步操作 = 阻塞主线程;
限制每次操作数量 + 分批执行 = 提高性能和用户体验。
推荐时间设置
场景 | 推荐 interval 值 | 原因 |
---|
一般业务场景(清理缓存、Token、临时存储) | 1000ms ~ 5000ms (1~5 秒) | 响应及时,用户几乎感觉不到延迟;清理频率适中 |
非频繁更新场景(只需定期清理,比如表单草稿) | 10000ms ~ 60000ms (10 秒 ~ 1 分钟) | 节省性能、CPU 空转少 |
极低性能开销要求(移动端、低性能设备) | 30000ms ~ 120000ms (30 秒 ~ 2 分钟) | 更节能,适合不那么敏感的业务 |
优化代码:
可以添加一个待删除队列,一次遍历所有的待处理的localstore
数据,分批处理,同时保证遍历到所有的数据
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| const expiredKeyQueue = [];
function scanExpiredKeys() { for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); try { const item = JSON.parse(localStorage.getItem(key)); if ( item?.expire && Date.now() > item.expire && !expiredKeyQueue.includes(key) ) { expiredKeyQueue.push(key); } } catch {} } }
function startCleaner(interval = 1000, limit = 5) { setInterval(() => { if (expiredKeyQueue.length === 0) scanExpiredKeys();
for (let i = 0; i < limit && expiredKeyQueue.length; i++) { const key = expiredKeyQueue.shift(); localStorage.removeItem(key); } }, interval); }
|
两者结合优化
使用两者结合,同时使用历览器的 requestIdleCallback 的这个 API,允许在浏览器空闲时调度后台任务运行,可以显着的提升用户的体验
实现 localStorage
过期数据的自动清理功能,我们可以将 requestIdleCallback
与 setTimeout
相结合,从而在 浏览器空闲时优先执行任务,同时也确保在不支持 requestIdleCallback
的环境中任务仍可执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103
| const UsertStorage = (() => { const PREFIX = "__user_storage__"; const CLEAN_INTERVAL_MIN = 2000; const CLEAN_LIMIT = 5; let cleanTimer = null;
function set(key, value, ddl = 0) { const Now_time = Date.now(); const data = { value, timestamp: Now_time, expires: ddl ? Now_time + ddl : 0, }; localStorage.setItem(PREFIX + key, JSON.stringify(data)); }
function get(key) { const data_json = localStorage.getItem(PREFIX + key); if (!data_json) return null;
try { const data = JSON.parse(data_json); const Now_time = Date.now();
if (data.expires && Now_time > data.expires) { localStorage.removeItem(PREFIX + key); return null; }
return data.value; } catch (event) { console.error("[UsertStorage] 解析失败", event); localStorage.removeItem(PREFIX + key); return null; } }
function remove(key) { localStorage.removeItem(PREFIX + key); }
function clean(limit = CLEAN_LIMIT) { const store_keys = Object.keys(localStorage).filter((key) => key.startsWith(PREFIX) ); let removed = 0; const Now_time = Date.now();
for (let i = 0; i < store_keys.length && removed < limit; i++) { try { const data = JSON.parse(localStorage.getItem(store_keys[i])); if (data.expires && Now_time > data.expires) { localStorage.removeItem(store_keys[i]); removed++; } } catch (event) { console.error("[UsertStorage] 解析失败", event); localStorage.removeItem(store_keys[i]); removed++; } } }
function startAutoClean(interval = CLEAN_INTERVAL_MIN) { if (cleanTimer) return;
const schedule = () => { if ("requestIdleCallback" in window) { requestIdleCallback(() => { clean(); cleanTimer = setTimeout(schedule, interval); }); } else { clean(); cleanTimer = setTimeout(schedule, interval); } }; schedule(); }
function stopAutoClean() { clearTimeout(cleanTimer); cleanTimer = null; }
return { set, get, remove, clean, startAutoClean, stopAutoClean, }; })();
|