【VueUse】useStorage让浏览器的Storage具有响应式
这是我开设第一个系列文章,旨在提升自己的源码阅读和调试能力、学习到更多的代码语法和逻辑。因为我是个TypeScript 新手,我会在末尾对遇到的 TS 语法进行总结,十分建议初学 TS 的同学阅读这篇文章,配合源码使用更佳。此系列文章对不懂 TS 的人员也能无障碍阅读,欢迎大家点赞收藏。
如何调试
- 克隆最新的 VueUse 仓库
git clone https://github.com/vueuse/vueuse.git
- 安装依赖,因为依赖管理器配置的是
pnpm
,所以使用pnpm
来安装依赖pnpm install
- 启动项目
pnpm dev
- 如果没有意外的话,就能在浏览器上看到运行在本地的官方文档页面
- 在
/packages/core
文件夹下找到想要调试的目标方法,在 VSCode 和浏览器源代码中设置断点
接下来就可以开始愉快的调试了!
系列文章
TS 语法介绍
在阅读时可能会涉及到一些 TS 中的基本语法,我先在这做个介绍,后面有不理解的可以跳转到这查看,TS 大神可直接跳过。
T
:是泛型的一个约定俗成的名称,它代表 Type(类型) 的缩写。泛型是指在定义函数、类或接口时不预先指定具体的类型,而是在使用时再指定类型。通过泛型,可以更加灵活地编写可重用的代码。keyof
:用于获得某个类型的所有可索引属性名称的联合类型。例如,如果有一个对象类型Person
,它有两个属性name
和age
,那么keyof Person
的结果就是name | age
,即一个字符串字面量类型的联合类型as
:用于将一个表达式的类型指定为另外一种类型。在编写代码时,有时候需要把一个变量或表达式的类型转换为我们期望的类型,这时候就可以使用as
关键字进行类型断言。Omit
:是 TS 中的一个工具类型,用于从已有类型中删除指定的属性,并返回剩余的那些属性所构成的新类型。Partial<T>
:是 TS 中的一个工具类型,用于将T类型中的所有属性都变为可选,把原来每个属性的类型都变成原类型和null | undefined
的联合类型。Record<K, T>
:类型别名,用于定义一个由指定类型的键和值组成的对象类型,表示对象的键名为K
类型,键值为T
类型。- 函数重载:是指可以定义一组具有相同名称但参数数量或类型不同的函数实现。在调用这个函数时,TS 编译器会根据传递的参数自动选择并调用匹配的重载函数。
- 泛型约束:用于限制泛型类型
T
参数的类型范围,从而提高代码的类型安全性。泛型约束可以通过关键字 extends 来实现
useStorage
用途:创建一个响应式变量,该变量会自动与 LocalStorage 或 SeesionStorage 同步
使用方法
import { useStorage } from '@vueuse/core'
// object 类型
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })
// boolean 类型
const flag = useStorage('my-flag', true) // returns Ref<boolean>
// number 类型
const count = useStorage('my-count', 0) // returns Ref<number>
// SessionStorage 保存数据
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>
// 从浏览器存储中删除数据
state.value = null
参数解析
源码位于/packages/core/useStorage/index.ts
,在这一段源码中就用到了 TS 中的泛型、函数重载和泛型约束,不懂可看上面章节👆。话不多说,先看参数。
对于不懂 TS 的来说,有些地方需要解释下:
- 给函数 useStorage 添加类型参数时,使用了泛型约束:
<T extends(string | number | boolean | object | null)>
,表示泛型参数T
必须是这些类型的其中之一,同时函数里面的泛型参数T
也会受到这样的约束,例如MaybeRefOrGetter<T>
、UseStorageOptions<T>
。 - 为什么文件导出多个同名的
useStorage
?这里用到了 TS 里的函数重载,直接看最后一个即可。
useStorage
接受四个参数,key
为必传参数,其他的为可选参数
- key:保存在本地存储中的键名,用于读取和写入数据。
- defaults:数据默认值,如果本地存储中没有数据时,则使用默认值。类型为
MaybeRefOrGetter
,类型定义代码如下,总结就是可能是 Ref 类型的值,可能是具体的 T 类型的值,或者是一个返回 T 类型的函数。
// 从 vue-demi 模块中导入 Ref 类型的定义
// vue-demi 能够允许编写 Vue2 和 Vue3 的通用库
import type { Ref } from 'vue-demi'
// 定义类型别名 MaybeRef<T>,表示可以是 T 类型或是 Ref<T> 类型
// 其中 T 是泛型类型参数,如果值为 Ref 类型则直接使用,否则默认当成常规类型来使用
export type MaybeRef<T> = T | Ref<T>
// 定义类型别名 MaybeRefOrGetter<T>,表示可以是 MaybeRef<T> 类型或者一个返回 T 类型的函数
// 如果传递的参数是函数,则返回值必须符合类型 T
export type MaybeRefOrGetter<T> = MaybeRef<T> | (() => T)
- storage:指定使用的存储类型,
localStorage
或sessionStorage
,默认为localStorage
,参数类型为StorageLike
或undefined
,定义StorageLike
代码如下,表示传递storage
对象上必须要有getItem()
、setItem()
和removeItem()
这三个方法。
export interface StorageLike {
getItem(key: string): string | null
setItem(key: string, value: string): void
removeItem(key: string): void
}
- options:可选的配置对象,UseStorageOptions 类型,下面在关键逻辑解析部分会涉及到这里的配置。
export interface UseStorageOptions<T> extends ConfigurableEventFilter, ConfigurableWindow, ConfigurableFlush {
/**
* Watch for deep changes
* 是否深度监听数据变化
*
* @default true
*/
deep?: boolean
/**
* Listen to storage changes, useful for multiple tabs application
* 监听存储变化
*
* @default true
*/
listenToStorageChanges?: boolean
/**
* Write the default value to the storage when it does not exist
* 当默认值不存在时是否将其写入存储
*
* @default true
*/
writeDefaults?: boolean
/**
* Merge the default value with the value read from the storage.
* 当设置了默认值并 storage 中的 key 已存在的情况下,是否将数据进行合并
* 为false 时,storage 中的数据会覆盖设置的默认值
* 为true 时,会合并数据,若 storage 中的数据和默认值存在相同的属性,则采用 storage 中的数据
*
* When setting it to true, it will perform a **shallow merge** for objects.
* 为 true 时,对于对象来说只是进行浅合并
*
* You can pass a function to perform custom merge (e.g. deep merge), for example:
* 可以传一个函数可以进行自定义合并
*
* @default false
*/
mergeDefaults?: boolean | ((storageValue: T, defaults: T) => T)
/**
* Custom data serialization
* 自定义数据序列化,查看定义 Serializer 接口代码可知
* serializer 对象需要有 read 方法和 write 方法
*/
serializer?: Serializer<T>
/**
* On error callback
* 发生错误时的回调函数
*
* Default log error to `console.error`
*/
onError?: (error: unknown) => void
/**
* Use shallow ref as reference
* 是否使用 shallowRef 进行引用
*
* @default false
*/
shallow?: boolean
}
主要逻辑
代码概览
下面我把里面一部分函数内代码先隐藏,先看看大概的逻辑,后面再对具体的功能点进行讲解
export function useStorage<T extends(string | number | boolean | object | null)>(
key: string,
defaults: MaybeRefOrGetter<T>,
storage: StorageLike | undefined,
options: UseStorageOptions<T> = {},
): RemovableRef<T> {
const {
flush = 'pre',
deep = true,
listenToStorageChanges = true,
writeDefaults = true,
mergeDefaults = false,
shallow,
window = defaultWindow,
eventFilter,
onError = (e) => {
console.error(e)
},
} = options
const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>
if (!storage) {
try {
storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()
}
catch (e) {
onError(e)
}
}
if (!storage)
return data
const rawInit: T = toValue(defaults)
const type = guessSerializerType<T>(rawInit)
const serializer = options.serializer ?? StorageSerializers[type]
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
data,
() => write(data.value),
{ flush, deep, eventFilter },
)
if (window && listenToStorageChanges) {
useEventListener(window, 'storage', update)
useEventListener(window, customStorageEventName, updateFromCustomEvent)
}
update()
return data
function write(v: unknown) {
...
}
function read(event?: StorageEventLike) {
...
}
function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {
...
}
function update(event?: StorageEventLike) {
...
}
}
shallowRef 和 Ref
通过options
中的shallow
参数来控制数据为 ref 类型还是 shallowRef 类型,并且最后使用as
将其转换为RemovableRef<T>
。
const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>
RemovableRef<T>
对Ref<T>
接口类型进行了扩展,代码如下:
export type RemovableRef<T> = Omit<Ref<T>, 'value'> & {
get value(): T
set value(value: T | null | undefined)
}
- 使用
Omit
从Ref<T>
中剔除value
属性; - 增加了
get value()
,返回T
类型; - 增加了
set value(value: T | null | undefined)
方法,用于设置value
值,可以接受T
类型、null
类型或undefined
类型的参数;
获取缓存对象
if (!storage) {
try {
// 获取 storage 对象
storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()
}
catch (e) {
onError(e)
}
}
// 若当前环境没有 storage 对象,则直接返回 ref 或 shallowref 格式的初始值
if (!storage)
return data
在这里如果没有指定缓存类型,会使用默认的 localStorage,源码这里对服务端渲染的项目做了处理,我简单的说下getSSRHandler()
函数的作用:
- 首先会检查全局对象是有没有
__vueuse_ssr_handlers__
属性,此属性应该是服务端渲染时会添加到全局对象上的。 - 如果没有此对象的话就返回
getSSRHandler()
函数的第二个参数,也就是window.localStorage
,获取storage
对象。 - 发生错误时执行
onError()
回调函数。
数据序列化
// 获取默认数据
const rawInit: T = toValue(defaults)
// 获取数据类型
const type = guessSerializerType<T>(rawInit)
// 序列化器,若没有自己定义 options.serializer,则直接根据数据类型去选择默认的数据序列化器
const serializer = options.serializer ?? StorageSerializers[type]
toValue()
- 函数意图很清晰,如果参数是函数的话就返回函数的返回值,不是函数的话就调用 Vue 中的unref()
函数并返回。guessSerializerType()
- 获取默认数据类型
export function guessSerializerType<T extends(string | number | boolean | object | null)>(rawInit: T) {
return rawInit == null
? 'any'
: rawInit instanceof Set
? 'set'
: rawInit instanceof Map
? 'map'
: rawInit instanceof Date
? 'date'
: typeof rawInit === 'boolean'
? 'boolean'
: typeof rawInit === 'string'
? 'string'
: typeof rawInit === 'object'
? 'object'
: !Number.isNaN(rawInit)
? 'number'
: 'any'
}
StorageSerializers
- 默认数据序列化器,里面定义不同类型的数据如何进行序列化和反序列化,Record 关键词上面有介绍👆
export interface Serializer<T> {
read(raw: string): T
write(value: T): string
}
export const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set' | 'date', Serializer<any>> = {
boolean: {
read: (v: any) => v === 'true',
write: (v: any) => String(v),
},
object: {
read: (v: any) => JSON.parse(v),
write: (v: any) => JSON.stringify(v),
},
number: {
read: (v: any) => Number.parseFloat(v),
write: (v: any) => String(v),
},
any: {
read: (v: any) => v,
write: (v: any) => String(v),
},
string: {
read: (v: any) => v,
write: (v: any) => String(v),
},
map: {
read: (v: any) => new Map(JSON.parse(v)),
write: (v: any) => JSON.stringify(Array.from((v as Map<any, any>).entries())),
},
set: {
read: (v: any) => new Set(JSON.parse(v)),
write: (v: any) => JSON.stringify(Array.from(v as Set<any>)),
},
date: {
read: (v: any) => new Date(v),
write: (v: any) => v.toISOString(),
},
}
事件监听
// 当前环境为浏览器并且 options.listenToStorageChanges 为 true
if (window && listenToStorageChanges) {
// 监听 localStorage 数据变化事件
useEventListener(window, 'storage', update)
// 监听自定义事件 vueuse-storage
useEventListener(window, customStorageEventName, updateFromCustomEvent)
}
在这里面不对useEventListener
进行解析,下一篇文章再来详细介绍它。
监听数据变化
参数options
中有个隐藏的参数eventFilter
,在useStorage
文档中并没有标注。但是在下面使用到了这个参数。VueUse官方文档中配置项章节 Event Filters 有介绍,并且有示例参考。
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
data,
() => write(data.value),
{ flush, deep, eventFilter },
)
pausableWatch
的源码有点复杂,官方文档戳我,在这里就不详细介绍(其实我也没看懂,只能知道到该是干嘛的😅),大家感兴趣的话可以自行查看,在这里我介绍下它的作用:
- 内部通过 Vue3 中的
watch
API 对data
进行监听,数据发生变化时执行回调函数。 - 通过传递
eventFilter
参数在可以执行回调函数之前可以对事件进行处理,/packages/shared/utils/filters
中已内置防抖和节流。 - 可以通过返回对象中的
pause
、resume
方法来控制数据发生变化时回调函数暂停、恢复执行。
write 方法
用于将数据写入Storage
function write(v: unknown) {
try {
// 若新数据为 null,则从 Storage 中删除
if (v == null) {
storage!.removeItem(key)
}
else {
// 数据序列化
const serialized = serializer.write(v)
// 获取 Storage 中的旧数据
const oldValue = storage!.getItem(key)
// 若新数据与旧数据不相等,才更新 Storage 中的数据
if (oldValue !== serialized) {
storage!.setItem(key, serialized)
// 判断是否为浏览器环境
if (window) {
// 触发自定义事件 vueuse-storage
window.dispatchEvent(new CustomEvent<StorageEventLike>(customStorageEventName, {
// 传递数据
detail: {
key,
oldValue,
newValue: serialized,
storageArea: storage!,
},
}))
}
}
}
}
catch (e) {
onError(e)
}
}
read 方法
读取缓存中的最新数据,其中使用到了options
中几个配置项:writeDefaults
和mergeDefaults
// 当初始化时执行update()方法,参数 event 才为 undefined;其他情况时,event 的类型为 StorageEventLike
function read(event?: StorageEventLike) {
// 初始化时从 Storage 中取数据,否则从事件信息 event 中获取最新数据
const rawValue = event
? event.newValue
: storage!.getItem(key)
// 当 Storage 中不存在数据时
if (rawValue == null) {
// 默认值 rawInit 不为 null 并且设置了 options.writeDefaults 为 true
if (writeDefaults && rawInit !== null)
// 将默认值写入 Storage
storage!.setItem(key, serializer.write(rawInit))
// 返回默认值
return rawInit
}
// 当初始化、 options.writeDefaults 为 true并且 Storage 已保存有数据时
// 将默认值 rawInit 与 Storage 中的数据进行合并
else if (!event && mergeDefaults) {
// 将数据进行反序列化
const value = serializer.read(rawValue)
// 调用自定义合并函数
if (typeof mergeDefaults === 'function')
return mergeDefaults(value, rawInit)
// 当默认数据值类型为 object,并且 Storage 中的数据反序列化之后不是数组
else if (type === 'object' && !Array.isArray(value))
// 对象合并
return { ...rawInit as any, ...value }
// 其他类型时直接返回 Storage 中的数据
return value
}
// 非初始化且 event 中的数据不是 string 类型
else if (typeof rawValue !== 'string') {
return rawValue
}
// 非初始化且 event 中的数据为 string 类型
else {
// 返回返序列化后的数据
return serializer.read(rawValue)
}
}
update 方法
当Storage
中的数据发生变化时会执行该方法,用于更新useStorage
返回的响应式数据。初始化时也会执行该方法,但是不会传递event
参数。
function update(event?: StorageEventLike) {
// 如果发生变化的 Storage 是否和设置的 Storage 一致
if (event && event.storageArea !== storage)
return
// Storage 数据全部清空时
if (event && event.key == null) {
data.value = rawInit
return
}
// Storage 中数据发生变化的键名是否和用来保存数据的键名一致
if (event && event.key !== key)
return
// 只有初始化时才能执行到这里
// 暂时 data 发生变化时执行的回调函数
pauseWatch()
try {
// 此时 event 为 undefined
data.value = read(event)
}
catch (e) {
onError(e)
}
finally {
// 恢复数据变化时执行回调函数
// 使用 nextTick 防止无线循环
if (event)
nextTick(resumeWatch)
else
resumeWatch()
}
}
updateFromCustomEvent 方法
触发自定义事件vueuse-storage
时执行的回调。
function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {
// 调用 update 方法,并将保存在自定义事件中的数据传入
update(event.detail)
}
useLocalStorage 和 useSessionStorage
这两个工具函数内部使用的就是useStorage
,不用传递storage
参数,只有key
、initialValue
和options
三个参数。useLocalStorage
内部使用的是window.localStorage
,useSessionStorage
内部使用的是window.sessionStorage
。
总结
实现响应式 LocalStorage 和 SesionStorage 原理就是通过 Vue 中的 watch 去监听数据变化,并在数据变化时将其写入Storage
中;同时也通过监听storage
事件和自定义事件来实现Storage中数据变化时更新响应式数据。其中还内置了数据的序列化以及返回序列化功能,用于 Storage 中的数据存储与获取。
其实实现这个功能不算复杂,大部分人对于实现都会有思路,但是 VueUse 中的 useStorage 在兼容性和扩展性上值得大家学习。
共同成长,无限进步!!! 💪
转载自:https://juejin.cn/post/7238769796351328316