实现可过期的 localstorage 数据

实现可过期的 localstorage 数据

localStorage 默认不会过期,数据会一直保留,除非用户手动清除。因此要实现 具有过期机制的 localStorage 缓存

主要有两种解决办法:

  1. 惰性删除
  2. 定时删除

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
// 传入的experies是秒数
function setItem(key, value, expires) {
const data = {
value: value,
// 注意:这里的Date.now()返回的是毫秒数, 所以乘以1000转换为秒数
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 = []; // 要删除的key
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);
}

问题:

  1. 为什么每次最多清除固定数量的 5 个?

    原因是:JavaScript 是单线程的,这样操作为了防止防阻塞主线程

    JavaScript 在浏览器中运行在主线程上,这条主线程负责:

    • 执行 JavaScript 代码;
    • 处理用户交互(点击、滚动等);
    • 页面渲染(布局、绘制等);
    • 网络请求回调等。

    如果我们在主线程上执行了耗时的操作,比如:

    • 遍历大量的 localStorage 数据;
    • 进行 JSON 解析(JSON.parse);
    • 判断过期、删除数据(localStorage.removeItem);

    那么整个主线程就会“卡住”,UI 渲染和用户响应都会变慢或暂时停止,用户会感觉页面卡顿或无响应。

    其次:

    localStorage 操作是同步的

    • localStorage.getItem()setItem()removeItem() 等方法都是同步阻塞式的
  2. 为什么限制“每次删除 5 个”可以缓解阻塞?

    每次只处理少量数据(例如 5 个),每轮清理的耗时可控(比如 < 16ms,即 1 帧时间),不会卡顿。

  3. 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 过期数据的自动清理功能,我们可以将 requestIdleCallbacksetTimeout 相结合,从而在 浏览器空闲时优先执行任务,同时也确保在不支持 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__"; // 存储的key前缀
const CLEAN_INTERVAL_MIN = 2000; // 最短2秒清一次
const CLEAN_LIMIT = 5; // 清除数量限制
let cleanTimer = null;

// 包装后的 set 方法,添加时间戳和过期时间
function set(key, value, ddl = 0) {
const Now_time = Date.now();
const data = {
value,
timestamp: Now_time,
expires: ddl ? Now_time + ddl : 0, // 0 表示不过期
};
localStorage.setItem(PREFIX + key, JSON.stringify(data));
}

// 包装后的 get 方法,带惰性删除逻辑
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;
}
}

// 删除 key
function remove(key) {
localStorage.removeItem(PREFIX + key);
}

// 清理过期项(最多 limit 个)
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 这个 API,则使用它来节省 CPU 资源
// requestIdleCallback 是一个浏览器 API,允许在浏览器空闲时调度后台任务运行。
requestIdleCallback(() => {
clean();
cleanTimer = setTimeout(schedule, interval);
});
} else {
// 如果浏览器不支持, 则使用 setTimeout 定时清理
clean();
cleanTimer = setTimeout(schedule, interval);
}
};
schedule();
}

// 停止定时清理器, 一般在页面卸载时调用
function stopAutoClean() {
clearTimeout(cleanTimer);
cleanTimer = null;
}

return {
set,
get,
remove,
clean,
startAutoClean,
stopAutoClean,
};
})();