1.4 State – useStorage
https://vueuse.org/core/useStorage/
作用
创建一个能访问和修改 LocalStorage or SessionStorage
的响应式变量。默认是localStorage
,可以通过参数进行修改。
官方示例
- 支持多种数据格式
import { useStorage } from '@vueuse/core'
// bind object
const state = useStorage('my-store', { hello: 'hi', greeting: 'Hello' })
// bind boolean
const flag = useStorage('my-flag', true) // returns Ref<boolean>
// bind number
const count = useStorage('my-count', 0) // returns Ref<number>
// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref<string>
// delete data from storage
state.value = null
- 合并默认值
默认情况下,useStorage
会使用storage
中的值(如果存在的话),并且会忽略用户传递的默认值。请注意,当你向默认值添加更多属性时,如果客户端的存储没有该key
值,则该key
可能会变成undefined
。
localStorage.setItem('my-store', '{"hello": "hello"}')
const state = useStorage('my-store', { hello: 'hi', greeting: 'hello' }, localStorage)
console.log(state.greeting) // undefined, 因为greeting这个key开始是并不存在
为了解决这种问题,需要使用mergeDefaults
属性
localStorage.setItem('my-store', '{"hello": "nihao"}')
const state = useStorage(
'my-store',
{ hello: 'hi', greeting: 'hello' },
localStorage,
{ mergeDefaults: true } // <--
)
console.log(state.hello) // 'nihao', from storage
console.log(state.greeting) // 'hello', from merged default value
当将其设置为true时,它将对对象执行浅合并。你可以传递一个函数来执行自定义合并(例如深度合并),例如:
const state = useStorage(
'my-store',
{ hello: 'hi', greeting: 'hello' },
localStorage,
{ mergeDefaults: (storageValue, defaults) => deepMerge(defaults, storageValue) } // <--
)
- 自定义序列化
默认情况下,useStorage
将根据提供的默认值的数据类型智能地使用相应的序列化器。比如,对象会使用JSON.stringify/ JSON.parse
,数字会使用Number.toString/parseFloat
。
你可以提供自定义的序列化函数。
import { useStorage } from '@vueuse/core'
useStorage(
'key',
{},
undefined,
{
serializer: {
read: (v: any) => v ? JSON.parse(v) : null,
write: (v: any) => JSON.stringify(v),
},
},
)
请注意,当你提供null作为默认值时,useStorage
不能推断它的数据类型。在这种情况下,你可以提供自定义序列化器或显式地重用内置序列化器。
import { StorageSerializers, useStorage } from '@vueuse/core'
const objectLike = useStorage('key', null, undefined, { serializer: StorageSerializers.object })
objectLike.value = { foo: 'bar' }
源码分析
地址:https://github.com/vueuse/vueuse/blob/main/packages/core/useStorage/index.ts
要搞清楚源码的实现,只需要搞懂一下几种场景:
- 初始化,包括合并默认项是如何做的。
- 取值逻辑
- 设置值的逻辑,主要是如何通过设置值,响应式的修改
storage
中的值
- 先看初始化的逻辑:
export function useStorage<T extends(string | number | boolean | object | null)>(
key: string,
defaults: MaybeComputedRef<T>,
storage: StorageLike | undefined,
options: UseStorageOptions<T> = {},
): RemovableRef<T> {
const {
// ......
} = options
// 1 因为没有传递shallow参数,因此这句话等价于 const data = ref(defaults)
const data = (shallow ? shallowRef : ref)(defaults) as RemovableRef<T>
// 2 如果没有设置storage,默认使用localStorage
if (!storage) {
try {
storage = getSSRHandler('getDefaultStorage', () => defaultWindow?.localStorage)()
}
catch (e) {
onError(e)
}
}
// 如果取不到storage,那直接返回。保存在本地失败。
if (!storage)
return data
/**
* 3 这里的目的是获取初始值的类型,来智能地选择序列化器
*/
const rawInit: T = resolveUnref(defaults)
// 如何获取类型?见下面代码
const type = guessSerializerType<T>(rawInit)
// 自定义序列化器优先级更高,如果没有,那么使用默认的。默认的序列化器见下面代码
const serializer = options.serializer ?? StorageSerializers[type]
/**
* pausableWatch 是一个可以暂停的watch函数,具体实现可以看 useRefHistory的解释
* 作用:监听data的变化,如果data发生了改变,就执行回调函数 () => write(data.value)
* 最后的options基本是给 watch 使用的
*/
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
data,
() => write(data.value),
{ flush, deep, eventFilter },
)
// 4 这句话就是 window.addEventListener('storage', update) 的意思,增加一个对 storage 的监听
// 这样,无论是响应式修改,还是直接修改localStorage,都能被监听到,触发update方法
if (window && listenToStorageChanges)
useEventListener(window, 'storage', update)
// 5 update方法在下面,开始时event为空,所以不会return。
update()
return data
function update(event?: StorageEvent) {
if (event && event.storageArea !== storage)
return
if (event && event.key == null) {
data.value = rawInit
return
}
if (event && event.key !== key)
return
// 6 先暂停对data的监听,同时读取一下存储过的值,最后恢复监听
// 暂停监听的目的:我们要对data.value赋值,如果不暂停,这个改变会被 pausableWatch 监听到,从而触发 cb
pauseWatch()
try {
data.value = read(event)
}
catch (e) {
onError(e)
}
finally {
// use nextTick to avoid infinite loop
if (event)
nextTick(resumeWatch)
else
resumeWatch()
}
}
function read(event?: StorageEvent) {
// 7 初始化场景event为空,因此rawValue就是storage中存储的值
const rawValue = event
? event.newValue
: storage!.getItem(key)
// 8 如果storage中没有存这个key对应的值,就把用户传递的默认值写入
if (rawValue == null) {
if (writeDefaults && rawInit !== null)
storage!.setItem(key, serializer.write(rawInit))
return rawInit
}
// 9 如果storage中已经有值,并且mergeDefaults存在
else if (!event && mergeDefaults) {
const value = serializer.read(rawValue)
// 9.1 如果mergeDefaults是一个函数,执行这个函数
if (isFunction(mergeDefaults))
return mergeDefaults(value, rawInit)
// 9.2 如果不是函数,而mergeDefaults又是存在的,默认mergeDefaults===true
// 这时候看用户传递的初始值,如果是对象,直接浅合并。
else if (type === 'object' && !Array.isArray(value))
return { ...rawInit as any, ...value }
// 9.3 都不满足,也就是用户给的默认值是基础类型,那么直接返回
return value
}
else if (typeof rawValue !== 'string') {
return rawValue
}
// 10 其他情况下,直接返回storage中存储的值。也就是默认情况下,如果storage有值,忽略用户传的值。
else {
return serializer.read(rawValue)
}
}
}
获取类型的代码如下,可以看到就是基本的instanceof
和typeof
方法
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'
}
默认的序列化器如下,是一个对象,key
是数据类型。
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(),
},
}
- 取值逻辑
比如在示例中const state = useStorage('vue-use-local-storage', theDefault)
,state已经是内存中的变量了,所以取值就是直接从state
对象中拿的。
- 设置值的逻辑
还是这个例子const state = useStorage('vue-use-local-storage', theDefault)
,这里的state
就是源码中返回的data
。看这个代码:
const { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
data,
() => write(data.value),
{ flush, deep, eventFilter },
)
当data
变化时,会触发write
方法,我们看一下这个方法。
function write(v: unknown) {
try {
// 1 如果设置的新值是null,那直接从storage中删除当前项
if (v == null) {
storage!.removeItem(key)
}
else {
// 2 先把新值序列化
const serialized = serializer.write(v)
// 3 取出原来的老值
const oldValue = storage!.getItem(key)
// 这里是序列化后进行比较,默认情况下就是字符串比较
if (oldValue !== serialized) {
// 4 新值和老值不一样,用新的替代老的
storage!.setItem(key, serialized)
// send custom event to communicate within same page
if (window) {
// 5 派发一个更新事件,这个事件会被 useEventListener(window, 'storage', update) 监听到
// 从而执行update方法,修改data的值
window?.dispatchEvent(new StorageEvent('storage', {
key,
oldValue,
newValue: serialized,
storageArea: storage as any,
}))
}
}
}
}
catch (e) {
onError(e)
}
}
问题一:在比较新值和老值不一致的情况下,已经对storage
赋值了,为啥还要派发事件?
原因是storage
事件只会发送给同源、而且处于打开状态的其他页面,而不会发送给触发改变的页面本身及处于关闭状态的页面。
在官方示例中,
const state = useStorage('vue-use-local-storage', theDefault)
const state2 = useStorage('vue-use-local-storage', theDefault)
假如执行了state.value.name = kk
,storage
中存储的值会发生改变,也就是说state
和storage
中的值都变了。
但是这个时候,state2
并不知道storage
中的值发生了变化,因为state
和state2
在同一个页面,storage
事件不会触发。所以需要手动派发事件,让state2
也响应式的改变。
问题二:在update
方法中,会执行data.value = read(event)
。也就是说:data
的变化,引发了更新,更新又再次修改了data
的值,这是为什么呢?那为什么在update
方法中需要执行read
方法呢?
原因是state和state2可能会有不同的序列化函数,执行read
方法,就可以让同一份数据以不同的反序列化方式被解析。