localStorage
默认不会过期,数据会一直保留,除非用户手动清除。因此要实现 具有过期机制的 localStorage 缓存
主要有两种解决办法:
-
惰性删除
-
定时删除
1.惰性删除
1.含义:
只有在读取某个键值时才判断其是否过期,若过期则删除该项并返回空值
2.实现思路
存储数据时,将数据和过期时间一起存入(如使用一个对象
{ value, expire }
)。获取数据时,检查
expire
是否早于当前时间,若过期则删除该项。
3.优缺点
优点:实现较为简单,不需要额外的定时器
缺点:如果莫一条数据一直没有被读取到,那么可能就不会被删除,会占用内存
4.代码实现
// 传入的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.代码实现
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)
}
问题:
-
为什么每次最多清除固定数量的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
数据,分批处理,同时保证遍历到所有的数据
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
的环境中任务仍可执行。
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
}
})()
添加注释等优化:
const UserStorage = (() => {
const PREFIX = '__QUPAI__'
const CLEAN_INTERVAL_MS = 2000 // 更改为MS,更清晰
const CLEAN_LIMIT = 5
let cleanTimer = null
/**
* 设置本地存储项。
* @param {string} key - 存储项的键名。
* @param {any} value - 存储项的值。
* @param {number} [expiresInMs=0] - 可选。过期时间,单位毫秒。如果为 0 或不提供,则永不过期。
*/
function set(key, value, expiresInMs = 0) {
const nowTime = Date.now()
const data = {
value,
timestamp: nowTime,
expires: expiresInMs ? nowTime + expiresInMs : 0
}
localStorage.setItem(PREFIX + key, JSON.stringify(data))
}
/**
* 获取本地存储项。
* @param {string} key - 存储项的键名。
* @returns {any | null} - 返回存储项的值,如果过期、不存在或解析失败则返回 null。
*/
function get(key) {
const dataJson = localStorage.getItem(PREFIX + key)
if (!dataJson) return null
try {
const data = JSON.parse(dataJson)
const nowTime = Date.now()
if (data.expires && nowTime > data.expires) {
localStorage.removeItem(PREFIX + key)
return null
}
return data.value
} catch (error) {
console.error(`[UserStorage] 解析键 \'${key}\' 失败:`, error)
localStorage.removeItem(PREFIX + key)
return null
}
}
/**
* 移除本地存储项。
* @param {string} key - 要移除存储项的键名。
*/
function remove(key) {
localStorage.removeItem(PREFIX + key)
}
/**
* 清理已过期的本地存储项。
* @param {number} [limit=CLEAN_LIMIT] - 可选。每次清理的最大数量。
*/
function clean(limit = CLEAN_LIMIT) {
const storeKeys = Object.keys(localStorage).filter((key) =>
key.startsWith(PREFIX)
)
let removed = 0
const nowTime = Date.now()
for (let i = 0; i < storeKeys.length && removed < limit; i++) {
const fullKey = storeKeys[i]
try {
const data = JSON.parse(localStorage.getItem(fullKey))
if (data.expires && nowTime > data.expires) {
localStorage.removeItem(fullKey)
removed++
}
} catch (error) {
console.error(`[UserStorage] 解析键 \'${fullKey}\' 失败:`, error)
localStorage.removeItem(fullKey)
removed++
}
}
}
/**
* 启动定时自动清理器。
* @param {number} [interval=CLEAN_INTERVAL_MS] - 可选。清理间隔时间,单位毫秒。
*/
function startAutoClean(interval = CLEAN_INTERVAL_MS) {
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
}
})()
export default UserStorage