实现localStorage过期数据的自动清理

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

主要有两种解决办法:

  1. 惰性删除

  2. 定时删除

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)
 }

问题:

  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数据,分批处理,同时保证遍历到所有的数据

 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 的环境中任务仍可执行。

 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
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值