Code前端首页关于Code前端联系我们

Vue3里怎么正确watch监听sessionStorage变化?有哪些实用坑点要避开?

terry 2小时前 阅读数 19 #Vue

做前端开发的朋友,肯定遇到过跨组件、跨路由传递临时轻量数据的场景——比如用户搜索页输入的筛选条件,跳转到结果页还能复用;或者多步骤表单的第一步数据,存起来等全部填完再提交,这时候,大家第一反应可能就是用sessionStorage:它只在当前标签页/窗口生效,关闭就清空,不占本地磁盘大空间,完美契合这种“临时短生命周期同标签传递”的需求,不过用过Vue2的同学知道,Vue2不能直接watch sessionStorage,只能用StorageEvent或者封装一个响应式的localStorage/sessionStorage工具类;那到了Vue3,有Composition API加持,是不是就简单多了?但真用起来,好像又会遇到不少“明明监听了却没反应”“数据变了好几次只更新一次”的小问题?今天咱们就一步步拆解,把这个问题彻底搞明白。

首先得搞清楚:为什么不能直接watch原始的sessionStorage.getItem/setItem?

不管Vue2还是Vue3,核心原理都是“响应式系统”——只有被响应式系统“追踪”的数据,在变化时才会触发视图更新或者watch回调,那sessionStorage.getItem/setItem属于什么呢?是浏览器原生提供的Web Storage API,完全独立于Vue的响应式机制之外:你调用setItem存数据,Vue根本不知道这回事;调用getItem拿数据,拿到的也只是普通的字符串、数字(JSON.parse转的)、布尔值,不是Vue的ref或者reactive对象,自然不会触发任何响应式操作。

打个比方,Vue的响应式系统就像你家门口的快递柜,只有放在柜里的东西(ref/reactive数据),取件、存件(数据变化)才会给你发提醒(watch回调/视图更新);而sessionStorage就像你楼下超市的寄存柜,东西存那儿、取那儿,快递柜完全不搭边,自然不会有任何动静。

Vue3里监听sessionStorage的几种正确姿势,哪种最适合你?

既然知道了问题的根源——Web Storage不在Vue的响应式体系里,那解决思路就很清晰了:要么把Web Storage的数据“搬”到Vue的响应式体系里,要么给Web Storage的“存取操作”加一层“触发Vue响应式”的钩子,接下来就给大家介绍3种常用的方法,分别分析优缺点和适用场景。

封装一个带响应式的useSessionStorage Hook

这是目前Vue3生态里最推荐、最通用的做法,很多开源UI组件库或者工具库(比如VueUse)都有现成的实现,但咱们今天自己手写一个简单版的,既能理解原理,又能根据自己的项目需求改。

手写步骤拆解

  1. 引入必要的Vue3 API:用Composition API的话,肯定要ref/reactive、watchEffect/watch这些对吧?还要考虑JSON的序列化和反序列化,sessionStorage只能存字符串,所以存对象、数组的时候要转,取的时候要转回来。
  2. 初始化响应式数据:当组件或者Hook被调用的时候,先从sessionStorage里读一遍初始数据,转成目标类型后赋值给ref/reactive对象——这样一开始就把数据“搬”进了Vue的快递柜。
  3. 监听响应式数据的变化,同步到sessionStorage:用watch或者watchEffect监听刚才那个ref/reactive对象,一旦它变了,就立刻把它序列化成字符串存到sessionStorage里——这样Vue的快递柜有动静,楼下超市的寄存柜也同步更新。
  4. 监听同标签页内其他地方对sessionStorage的修改,同步回响应式数据:哦对了,有时候可能不是通过我们封装的Hook修改的sessionStorage,比如项目里还有旧代码用了原生的setItem,或者第三方库操作了同一个key,这时候怎么办?可以用浏览器原生的StorageEvent,监听window的storage事件,当事件的key是我们当前监听的sessionStorage的key时,就把event.newValue转成目标类型,更新响应式数据。

简单版useSessionStorage代码示例

import { ref, watchEffect } from 'vue'
export function useSessionStorage(key, defaultValue) {
  // 第一步:初始化响应式数据
  const storedValue = ref(null)
  // 先尝试从sessionStorage读取
  try {
    const rawValue = sessionStorage.getItem(key)
    storedValue.value = rawValue ? JSON.parse(rawValue) : defaultValue
  } catch (error) {
    // 如果JSON.parse出错了(比如存的不是合法JSON),就用默认值
    console.warn(`解析sessionStorage中key为${key}的数据失败:`, error)
    storedValue.value = defaultValue
  }
  // 第二步:监听响应式数据变化,同步到sessionStorage
  // 这里用watchEffect,因为不管是storedValue本身变,还是它里面的嵌套属性变(如果初始化为对象数组的话),都会触发
  // 但要注意,如果用ref存对象数组的话,watchEffect里要先深拷贝吗?不用,JSON.stringify本身就会遍历所有嵌套属性
  watchEffect(() => {
    try {
      // 如果当前值是undefined或者null,要不要删key?可以加个参数控制,这里简化为直接存
      sessionStorage.setItem(key, JSON.stringify(storedValue.value))
    } catch (error) {
      console.warn(`写入sessionStorage中key为${key}的数据失败:`, error)
    }
  })
  // 第三步:监听同标签页其他地方的修改(注意:StorageEvent在同标签页内用原生setItem才会触发吗?不对,原生setItem在同窗口不同标签页才会触发?哦等下,这个是个大坑!后面单独讲!)
  // 这里先写个通用的,不管是不是同标签页,都监听
  const handleStorageChange = (event) => {
    if (event.key === key && event.storageArea === sessionStorage) {
      try {
        storedValue.value = event.newValue ? JSON.parse(event.newValue) : defaultValue
      } catch (error) {
        console.warn(`解析sessionStorage中key为${key}的修改数据失败:`, error)
        storedValue.value = defaultValue
      }
    }
  }
  window.addEventListener('storage', handleStorageChange)
  // 第四步:组件卸载时移除事件监听,防止内存泄漏
  // 这里要注意,useSessionStorage是Hook,所以要用到onUnmounted API
  import { onUnmounted } from 'vue'
  onUnmounted(() => {
    window.removeEventListener('storage', handleStorageChange)
  })
  return storedValue
}

优缺点和适用场景

优点:

  1. 完全符合Vue3的Composition API风格,代码复用性强,哪里需要哪里调用;
  2. 双向同步:响应式数据变了同步到sessionStorage,(不同标签页的)sessionStorage变了同步回响应式数据;
  3. 支持多种数据类型:自动处理JSON的序列化和反序列化;
  4. 有基础的错误处理:JSON解析失败、写入失败都会有警告;
  5. 防止内存泄漏:组件卸载时自动移除事件监听。

缺点:

  1. 手写版的功能可能不够完善:比如没有删除key的方法(可以加个return的remove方法),没有控制是否同步初始化数据的参数,没有控制是否监听storage事件的参数,没有防抖节流(有时候数据频繁变化,比如搜索框实时输入,不需要每次都存sessionStorage);
  2. 刚才代码里提到的StorageEvent同标签页的坑,这个后面避坑部分会重点讲。

适用场景: 几乎所有需要在Vue3项目里用sessionStorage跨组件、跨路由传递临时数据的场景,不管是个人项目还是企业项目,都可以用这种方法——如果觉得手写麻烦,直接用VueUse里的useStorage或者useSessionStorage就行,功能更完善,还有类型支持(TypeScript的话)。

直接用VueUse的useSessionStorage(适合不想手写的朋友)

刚才方法一里提到了VueUse,这是一个专门为Vue3设计的Composition API工具库,里面有300+个常用的Hook,覆盖了状态管理、DOM操作、浏览器API、网络请求、动画等几乎所有前端开发场景,而且代码质量很高,有完善的TypeScript类型支持,很多开源项目都在用。

使用步骤

  1. 安装VueUse:npm i @vueuse/core 或者 yarn add @vueuse/core 或者 pnpm add @vueuse/core;
  2. 在组件或者Hook里引入并使用
<template>
  <div>
    <h2>搜索筛选条件</h2>
    <input v-model="searchKeyword" placeholder="请输入搜索关键词" />
    <select v-model="searchCategory">
      <option value="">全部分类</option>
      <option value="book">图书</option>
      <option value="clothes">服装</option>
      <option value="electronics">电子产品</option>
    </select>
    <p>当前筛选条件:关键词={{ searchKeyword }},分类={{ searchCategory }}</p>
  </div>
</template>
<script setup>
import { useSessionStorage } from '@vueuse/core'
// 第一个参数是sessionStorage的key
// 第二个参数是默认值(可以是任意JSON可序列化的类型)
// 第三个参数是可选配置项(后面会讲几个常用的)
const searchKeyword = useSessionStorage('search-keyword', '')
const searchCategory = useSessionStorage('search-category', '')
</script>

几个常用的可选配置项

VueUse的useSessionStorage第三个参数是一个配置对象,这里给大家介绍几个最常用的:

  1. mergeDefaults:布尔值,默认是false,如果默认值是一个对象,而sessionStorage里已经有这个key对应的对象了,mergeDefaults设为true的话,会把默认值和sessionStorage里的对象合并,而不是直接覆盖;
  2. deep:布尔值,默认是true,如果默认值是一个对象或者数组,deep设为true的话,会深度监听对象的嵌套属性变化,同步到sessionStorage;
  3. listenToStorage:布尔值,默认是true,控制是否监听window的storage事件(也就是其他标签页的修改);
  4. writeDefaults:布尔值,默认是false,如果sessionStorage里没有这个key对应的初始数据,writeDefaults设为true的话,会立刻把默认值写入sessionStorage;
  5. serializer:对象,自定义序列化和反序列化的方法,默认是JSON.stringify和JSON.parse,但有时候比如存Date对象,JSON.stringify转成字符串后,JSON.parse转回来还是字符串,这时候可以自定义serializer:
const mySerializer = {
  read: (raw) => {
    if (!raw) return null
    const parsed = JSON.parse(raw)
    // 把字符串类型的日期转成Date对象
    if (parsed.date && typeof parsed.date === 'string') {
      parsed.date = new Date(parsed.date)
    }
    return parsed
  },
  write: (value) => JSON.stringify(value)
}
const searchParams = useSessionStorage('search-params', { keyword: '', date: new Date() }, { serializer: mySerializer })

优缺点和适用场景

优点:

  1. 功能超级完善:刚才介绍的只是冰山一角,还有很多其他配置项,比如onError、onWrite、onRead这些钩子,甚至还有支持SSR的配置;
  2. TypeScript支持完美:类型定义非常详细,写代码的时候有自动补全,还能避免很多类型错误;
  3. 代码质量高:经过了大量开源项目的验证,Bug很少;
  4. 使用超级简单:不用手写复杂的逻辑,一行代码搞定。

缺点:

  1. 增加了项目的依赖:如果项目里只需要用useSessionStorage这一个Hook,可能觉得有点“大材小用”,不过VueUse支持Tree Shaking,打包的时候只会打包你用到的Hook,不用担心体积太大;
  2. 可能不太理解底层原理:不过没关系,新手可以先用起来,等有时间了再去看VueUse的源码,学习一下别人的写法。

适用场景: 不管是新手还是老手,只要不想手写useSessionStorage,都可以用VueUse的;如果项目里已经在用VueUse了,那更不用说了,直接用就行。

在根组件或者全局状态管理里封装一个响应式的对象,监听后同步到sessionStorage

这种方法适合项目里有很多地方需要用到sessionStorage的共享数据,比如用户的临时登录状态(不过登录状态一般用localStorage或者Vuex/Pinia)、全局的临时弹窗状态、全局的临时主题切换(不过主题切换一般用localStorage或者CSS变量+响应式数据)。

具体思路

  1. 在main.js/main.ts或者全局状态管理(Pinia/Vuex)里定义一个响应式的对象,比如叫globalSessionData;
  2. 用watchEffect或者watch深度监听这个globalSessionData对象,一旦它变了,就立刻把整个对象序列化成字符串存到sessionStorage的某个固定key里,比如叫global-session-data;
  3. 初始化的时候,先从sessionStorage里读global-session-data,转成对象后合并到globalSessionData里;
  4. 在其他组件里,直接引入并使用globalSessionData的属性,或者通过Pinia/Vuex的getters/actions来访问和修改。

用Pinia实现的简单示例

  1. 安装Pinia:npm i pinia 或者 yarn add pinia 或者 pnpm add pinia;
  2. 在main.js里注册Pinia
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
const pinia = createPinia()
app.use(pinia)
app.mount('#app')
  1. 创建一个Pinia store,叫useGlobalSessionStore
import { defineStore } from 'pinia'
import { watchEffect } from 'vue'
export const useGlobalSessionStore = defineStore('globalSession', {
  state: () => ({
    // 初始状态
    searchKeyword: '',
    searchCategory: '',
    multiStepFormStep1: {}
  }),
  actions: {
    // 可以定义一些修改状态的方法,比如重置筛选条件
    resetSearchParams() {
      this.searchKeyword = ''
      this.searchCategory = ''
    }
  },
  // Pinia的store有一个onMounted钩子吗?没有,不过可以在store定义外面写watchEffect,或者在store的state初始化后写
  // 这里在store定义外面写watchEffect
  setup() {
    const store = useGlobalSessionStore()
    // 初始化:从sessionStorage读取数据
    try {
      const rawValue = sessionStorage.getItem('global-session-data')
      if (rawValue) {
        const parsed = JSON.parse(rawValue)
        // 把读取到的数据合并到store的state里
        Object.assign(store.$state, parsed)
      }
    } catch (error) {
      console.warn('解析global-session-data失败:', error)
    }
    // 监听store的state变化,同步到sessionStorage
    watchEffect(() => {
      try {
        sessionStorage.setItem('global-session-data', JSON.stringify(store.$state))
      } catch (error) {
        console.warn('写入global-session-data失败:', error)
      }
    })
    return {}
  }
})
  1. 在组件里使用
<template>
  <div>
    <h2>搜索筛选条件(用Pinia)</h2>
    <input v-model="globalSessionStore.searchKeyword" placeholder="请输入搜索关键词" />
    <select v-model="globalSessionStore.searchCategory">
      <option value="">全部分类</option>
      <option value="book">图书</option>
      <option value="clothes">服装</option>
      <option value="electronics">电子产品</option>
    </select>
    <button @click="globalSessionStore.resetSearchParams">重置</button>
    <p>当前筛选条件:关键词={{ globalSessionStore.searchKeyword }},分类={{ globalSessionStore.searchCategory }}</p>
  </div>
</template>
<script setup>
import { useGlobalSessionStore } from '@/stores/globalSession'
const globalSessionStore = useGlobalSessionStore()
</script>

优缺点和适用场景

优点:

  1. 所有需要共享的sessionStorage数据都集中管理,代码结构清晰,维护起来方便;
  2. 可以利用Pinia/Vuex的其他功能,比如getters(计算派生状态)、actions(异步操作)、插件(比如持久化插件,不过我们这里自己写了持久化逻辑);
  3. 和Vue3的响应式系统完美结合,不用考虑太多双向同步的问题。

缺点:

  1. 增加了全局状态管理的依赖,如果项目里不需要用Pinia/Vuex做其他全局状态管理,可能觉得有点麻烦;
  2. 所有共享数据都存在一个固定的sessionStorage key里,如果数据量比较大,可能会有性能问题(不过sessionStorage的上限一般是5MB,临时数据不会太大,所以这个问题一般不用担心);
  3. 同样存在StorageEvent同标签页的坑

适用场景: 项目里有很多地方需要用到sessionStorage的共享数据,或者项目里已经在用Pinia/Vuex做其他全局状态管理的场景。

避坑!避坑!避坑!监听sessionStorage最容易踩的3个大坑

刚才讲方法的时候,已经提到了几个坑,比如StorageEvent同标签页的问题,现在咱们把最容易踩的3个大坑单独列出来,详细讲一下原因和解决方法。

坑一:StorageEvent在同标签页内用原生setItem修改sessionStorage,不会触发!

这个是最常见的一个坑,很多同学手写useSessionStorage的时候,都以为监听window的storage事件就能监听到所有地方的修改,但实际上,StorageEvent的触发条件是:在同一个浏览器的不同标签页/窗口,或者同一个标签页的不同iframe里,修改了同一个Storage对象(localStorage或者sessionStorage)——同标签页、同iframe里用原生setItem修改,是不会触发StorageEvent的!

那为什么会这样呢?其实这是浏览器的设计策略:同标签页内的代码,理论上应该是在同一个应用里,修改数据应该通过应用内部的状态管理或者响应式数据来传递,而不是通过StorageEvent——StorageEvent主要是用来解决跨标签页/窗口数据同步的问题的。

那如果项目里真的有旧代码用了原生setItem修改同一个sessionStorage的key,怎么办呢?有两种解决方法:

解决方法一:重写sessionStorage的setItem、removeItem、clear方法

既然原生的setItem等方法不会触发同标签页的StorageEvent,那我们可以自己重写这些方法,在重写的方法里手动触发一个自定义事件,或者手动调用我们封装的更新响应式数据的逻辑。

举个重写sessionStorage setItem方法的例子:

// 先保存原生的setItem方法
const originalSetItem = sessionStorage.setItem
// 重写setItem方法
sessionStorage.setItem = function(key, value) {
  // 先调用原生的setItem方法,把数据存进去
  originalSetItem.call(this, key, value)
  // 然后手动触发一个自定义事件,比如叫'session-storage-change'
  const event = new CustomEvent('session-storage-change', {
    detail: {
      key,
      newValue: value,
      storageArea: sessionStorage
    }
  })
  window.dispatchEvent(event)
}
// 同样的方法重写removeItem和clear
const originalRemoveItem = sessionStorage.removeItem
sessionStorage.removeItem = function(key) {
  originalRemoveItem.call(this, key)
  const event = new CustomEvent('session-storage-change', {
    detail: {
      key,
      newValue: null,
      storageArea: sessionStorage
    }
  })
  window.dispatchEvent(event)
}
const originalClear = sessionStorage.clear
sessionStorage.clear = function() {
  originalClear.call(this)
  const event = new CustomEvent('session-storage-change', {
    detail: {
      key: null,
      newValue: null,
      storageArea: sessionStorage
    }
  })
  window.dispatchEvent(event)
}

在我们手写的useSessionStorage Hook里,除了监听window的storage事件,还要监听window的'session-storage-change'自定义事件:

// 把刚才的handleStorageChange函数稍微改一下,让它能接受两种事件的detail
const handleStorageChange = (event) => {
  // 先判断是原生的StorageEvent还是自定义的session-storage-change事件
  const detail = event.detail || event
  if (detail.storageArea === sessionStorage) {
    // 如果detail.key是null,说明是clear操作,这时候可以不用处理每个key,或者根据自己的需求处理
    if (detail.key === key || detail.key === null) {
      try {
        storedValue.value = detail.newValue ? JSON.parse(detail.newValue) : defaultValue
      } catch (error) {
        console.warn(`解析sessionStorage中key为${key}的修改数据失败:`, error)
        storedValue.value = defaultValue
      }
    }
  }
}
// 监听两种事件
window.addEventListener('storage', handleStorageChange)
window.addEventListener('session-storage-change', handleStorageChange)
// 卸载时也要移除两种事件的监听
onUnmounted(() => {
  window.removeEventListener('storage', handleStorageChange)
  window.removeEventListener('session-storage-change', handleStorageChange)
})

不过要注意,重写原生的Web Storage API是有风险的:

  1. 可能会影响第三方库的正常工作:如果第三方库也依赖原生的setItem等方法的行为,重写后可能会出问题;
  2. 可能会被浏览器的安全策略拦截:不过一般情况下不会,除非浏览器有特别严格的安全设置;
  3. 代码可维护性可能会降低:如果团队里的其他同学不知道原生的API被重写了,可能会写出有问题的代码。

最好的解决方法还是:尽量不要在项目里混用原生的Web Storage API和我们封装的响应式Hook/Store,所有对sessionStorage的操作都通过我们封装的工具来完成

解决方法二:用VueUse的useSessionStorage

哦对了,VueUse的useSessionStorage已经帮我们解决了这个问题!它内部既监听了window的storage事件(跨标签页),又监听了window的自定义事件(同标签页的其他修改),不过具体是怎么实现的,大家可以去看VueUse的源码——这里就不剧透了,总之用VueUse的就对了。

坑二:JSON序列化和反序列化时的类型丢失问题

刚才讲方法的时候,提到了自定义serializer的问题,这里再详细讲一下:sessionStorage只能存字符串,所以我们存对象、数组、Date对象、RegExp对象、Map对象、Set对象这些JSON不能直接序列化的类型时,都会有类型丢失的问题——

  • Date对象:JSON.stringify转成"2024-05-20T12:00:00.000Z",JSON.parse转回来还是字符串;
  • RegExp对象:JSON.stringify转成{},JSON.parse转回来还是空对象;
  • Map对象:JSON.stringify转成{},JSON.parse转回来还是空对象;
  • Set对象:JSON.stringify转成{},JSON.parse转回来还是空对象;
  • undefined:JSON.stringify会直接忽略这个属性,或者如果整个值是undefined的话,会返回undefined,存到sessionStorage里的话,会变成"undefined"。

那怎么解决这个问题呢?有几种方法:

解决方法一:自定义serializer(适合常用的特殊类型,比如Date)

刚才方法二里已经给了一个自定义serializer处理Date对象的例子,这里再给一个处理RegExp、Map、Set的例子(不过这个例子比较复杂,一般情况下不推荐自己手写,除非项目里确实需要用到这些类型):

const mySerializer = {
  read: (raw) => {
    if (!raw) return null
    const parsed = JSON.parse(raw)
    // 递归处理特殊类型
    const reviver = (key, value) => {
      if (typeof value === 'object' && value !== null) {
        // 处理Date对象(有__type__属性为'Date',value属性为时间戳)
        if (value.__type__ === 'Date') {
          return new Date(value.value)
        }
        // 处理RegExp对象(有__type__属性为'Regexp',source属性为正则表达式的源字符串,flags属性为标志)
        if (value.__type__ === 'RegExp') {
          return new RegExp(value.source, value.flags)
        }
        // 处理Map对象(有__type__属性为'Map',entries属性为键值对数组)
        if (value.__type__ === 'Map') {
          return new Map(value.entries)
        }
        // 处理Set对象(有__type__属性为'Set',values属性为值数组)
        if (value.__type__ === 'Set') {
          return new Set(value.values)
        }
      }
      return value
    }
    return JSON.parse(raw, reviver)
  },
  write: (value) => {
    // 递归处理特殊类型
    const replacer = (key, value) => {
      if (typeof value === 'object' && value !== null) {
        // 处理Date对象
        if (value instanceof Date) {
          return { __type__: 'Date', value: value.getTime() }
        }
        // 处理RegExp对象
        if (value instanceof RegExp) {
          return { __type__: 'RegExp', source: value.source, flags: value.flags }
        }
        // 处理Map对象
        if (value instanceof Map) {
          return { __type__: 'Map', entries: Array.from(value.entries()) }
        }
        // 处理Set对象
        if (value instanceof Set) {
          return { __type__: 'Set', values: Array.from(value.values()) }
        }
      }
      return value
    }
    return JSON.stringify(value, replacer)
  }
}

解决方法二:尽量不要在sessionStorage里存JSON不能直接序列化的类型

这个是最简单、最稳妥的解决方法——sessionStorage本来就是用来存临时轻量数据的,尽量只存字符串、数字、布尔值、普通对象、普通数组这些JSON能直接序列化的类型,不要存Date、RegExp、Map、Set、Function这些特殊类型。

如果确实需要存Date对象,可以存时间戳(number类型)或者ISO字符串(string类型),然后在需要用的时候再转成Date对象;如果确实需要存RegExp对象,可以存源字符串和标志,然后在需要用的时候再转成RegExp对象;如果确实需要存Map或者Set对象,可以存键值对数组或者值数组,然后在需要用的时候再转成Map或者Set对象。

坑三:watchEffect的无限循环问题

刚才手写useSessionStorage的时候,用了watchEffect来监听响应式数据的变化,同步到sessionStorage——那有没有可能出现无限循环的问题呢?

  1. 响应式数据storedValue变了;
  2. watchEffect触发,把storedValue存到sessionStorage里;
  3. 如果我们刚才重写了sessionStorage的setItem方法,触发了自定义事件;
  4. 自定义事件触发handleStorageChange函数;
  5. handleStorageChange函数把sessionStorage里的新值转成对象,更新storedValue;
  6. 响应式数据storedValue又变了,回到步骤1,无限循环。

那怎么解决这个问题呢?其实很简单:在更新storedValue之前,先判断一下新值和旧值是不是一样的,如果一样的话,就不更新

举个例子,修改刚才手写useSessionStorage Hook里的handleStorageChange函数:

import { isRef, isReactive, toRaw } from 'vue'
// 先写一个深比较函数,判断两个值是不是一样的
// 这里用VueUse的isEqual的话会更简单,但如果是手写的话,可以自己写一个简单的深比较
const isEqual = (a, b) => {
  // 先判断类型
  if (typeof a !== typeof b) return false
  // 如果是基本类型,直接比较
  if (typeof a !== 'object' || a === null || b === null) return a === b
  // 如果是Date对象,比较时间戳
  if (a instanceof Date && b instanceof Date) return a.getTime() === b.getTime()
  // 如果是RegExp对象,比较源字符串和标志
  if (a instanceof RegExp && b instanceof RegExp) return a.source === b.source && a.flags === b.flags
  // 如果是数组,比较长度和每个元素
  if (Array.isArray(a) && Array.isArray(b)) {
    if (a.length !== b.length) return false
    for (let i = 0; i < a.length; i++) {
      if (!isEqual(a[i], b[i])) return false
    }
    return true
  }
  // 如果是普通对象,比较键的数量和每个键对应的值
  const keysA = Object.keys(a)
  const keysB = Object.keys(b)
  if (keysA.length !== keysB.length) return false
  for (const key of keysA) {
    if (!keysB.includes(key) || !isEqual(a[key], b[key])) return false
  }
  return true
}
const handleStorageChange = (event) => {
  const detail = event.detail || event
  if (detail.storageArea === sessionStorage) {
    if (detail.key === key || detail.key === null) {
      try {
        const newValue = detail.newValue ? JSON.parse(detail.newValue) : defaultValue
        // 先获取storedValue的原始值(因为ref/reactive对象是Proxy包装过的,toRaw可以拿到原始值)
        const oldRawValue = isRef(storedValue) ? toRaw(storedValue.value) : (isReactive(storedValue) ? toRaw(storedValue) : storedValue)
        // 深比较新值和旧值,如果不一样的话,才更新
        if (!isEqual(oldRawValue, newValue)) {
          storedValue.value = newValue
        }
      } catch (error) {
        console.warn(`解析sessionStorage中key为${key}的修改数据失败:`, error)
        const oldRawValue = isRef(storedValue) ? toRaw(storedValue.value) : (isReactive(storedValue) ? toRaw(storedValue) : storedValue)
        if (!isEqual(oldRawValue, defaultValue)) {
          storedValue.value = defaultValue
        }
      }
    }
  }
}

不过同样的,深比较函数也可以用VueUse的isEqual,不用自己手写,而且VueUse的isEqual功能更完善,支持更多特殊类型的比较。

刚才说的无限循环问题,用VueUse的useSessionStorage的话,也已经帮我们解决了,所以还是推荐用VueUse的。

总结一下

今天咱们讲了Vue3里怎么正确watch监听sessionStorage变化,有3种常用的方法:

  1. 封装一个带响应式的useSessionStorage Hook:适合想理解原理、或者需要根据项目需求定制的朋友;
  2. 直接用VueUse的useSessionStorage:最推荐的方法,功能完善,使用简单,有TypeScript支持;
  3. 在根组件或者全局状态管理里封装一个响应式的对象:适合项目里有很多共享sessionStorage数据的场景。

还讲了最容易踩的3个大坑:

  1. StorageEvent同标签页不触发:尽量不要混用原生API和封装的工具,或者重写原生API,或者用VueUse的;
  2. JSON序列化和反序列化的类型丢失:尽量不要存特殊类型,或者自定义serializer;
  3. watchEffect的无限循环:更新响应式数据之前先深比较新值和旧值。 能帮到大家,以后在Vue3项目里用sessionStorage的时候,不要再踩这些坑了!如果还有其他问题,欢迎在评论区留言讨论。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门