【源码阅读】如何把Storage封装成你用不起的样子?
离大谱,一个简单的Storage,愣是写了几百行代码,这你敢信?
在我的记忆里,Storage一共就三行代码。
localStorage.getItem()
localStorage.setItem()
localStorage.removeItem()
就这三行代码,还能写到天上去?
事实证明,还真可以。
这个写了几百行代码的函数,就是vueuse库的useStorag。
今天就来盘一盘useStorag的源码。
源码调试步骤。
// 下载源码
git clone https://github.com/vueuse/vueuse.git
// 安装依赖
pnpm install
// 启动文档
pnpm run dev
// 找到packages/core/useStorage/index.ts
先来看一下入参,以及作用。
key键指,只能是字符串。
defaults,这个就有意思了,localStorage.setItem的时候,只能设置字符串,但是useStorage可以传很多格式,比如ref,函数,对象,字符串都可以,这些在hook内部都做了兼容。
storage,可以是localStorage,sesessionStorage,或者是任意带getItem,setItem,removeItem的对象,默认为localStorage。
options,这个参数也很有意思,主要是一些配置,包括watch的配置,是否监听事件,是否使用shallow等。
具体可查看类型定义.
/**
* 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.
*
* When setting it to true, it will perform a **shallow merge** for objects.
* 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<T>
/**
* On error callback
*
* Default log error to `console.error`
*/
onError?: (error: unknown) => void
/**
* Use shallow ref as reference
*
* @default false
*/
shallow?: boolean
下面看一下hook的具体实现。
第一步,设置默认值,包括options默认值,data使用ref,storage使用localStorage。
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)
}
}
console.log('storage', storage ,data)
if (!storage)
return data
第二步,设置修改方法
取出defaults的值,并判断其类型,不同的类型对应不同的序列化方式。
const rawInit: T = toValue(defaults)
const type = guessSerializerType<T>(rawInit)
const serializer = options.serializer ?? StorageSerializers[type]
比如我想存一个数字2,那么存的时候,他会自动转成字符串2存起来,取的时候,自动转化成number类型返回。
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 { pause: pauseWatch, resume: resumeWatch } = pausableWatch(
data,
() => write(data.value),
{ flush, deep, eventFilter },
)
if (window && listenToStorageChanges) {
useEventListener(window, 'storage', update)
useEventListener(window, customStorageEventName, updateFromCustomEvent)
}
先说pausableWatch函数。
pausableWatch内部是调用了watch监听函数,同时兼容eventFilter。
怎么理解,这个eventFilter呢?
其实就是一个钩子函数,在触发watch监听的时候,调用一下。有了这个钩子函数,我们就可以监听值的变化,在值发生改变之后,做一些其他的操作。
还有一个,我觉得很赞的点,在于storage事件的尖挺。(useEventListener另外的一个hook就是在对象添加监听函数,这里理解成addEventListener就行)
库中还额外监听了一个自定义事件(vueuse-storage),在数据写入的时候触发事件。
if (window) {
window.dispatchEvent(new CustomEvent<StorageEventLike>(customStorageEventName, {
detail: {
key,
oldValue,
newValue: serialized,
storageArea: storage!,
},
}))
}
斯过哎(日语)。
第4步,调用update写入数据。
function update(event?: StorageEventLike) {
if (event && event.storageArea !== storage)
return
if (event && event.key == null) {
data.value = rawInit
return
}
if (event && event.key !== key)
return
pauseWatch()
try {
data.value = read(event)
}
catch (e) {
onError(e)
}
finally {
// use nextTick to avoid infinite loop
if (event)
nextTick(resumeWatch)
else
resumeWatch()
}
}
这里有个pauseWatch函数跟resumeWatch函数。
这两个函数的作用就是改变isActive的值,当isActive为false时,则eventFilte不触发。
export function pausableFilter(extendFilter: EventFilter = bypassFilter): Pausable & { eventFilter: EventFilter } {
const isActive = ref(true)
function pause() {
isActive.value = false
}
function resume() {
isActive.value = true
}
const eventFilter: EventFilter = (...args) => {
if (isActive.value)
extendFilter(...args)
}
return { isActive: readonly(isActive), pause, resume, eventFilter }
}
read(event)函数,真正设置Storage.setItem函数的地方
function read(event?: StorageEventLike) {
const rawValue = event
? event.newValue
: storage!.getItem(key)
if (rawValue == null) {
if (writeDefaults && rawInit !== null)
storage!.setItem(key, serializer.write(rawInit))
return rawInit
}
else if (!event && mergeDefaults) {
const value = serializer.read(rawValue)
if (typeof mergeDefaults === 'function')
return mergeDefaults(value, rawInit)
else if (type === 'object' && !Array.isArray(value))
return { ...rawInit as any, ...value }
return value
}
else if (typeof rawValue !== 'string') {
return rawValue
}
else {
return serializer.read(rawValue)
}
}
判断初始值是否存在,是否需要合并等边界情况,并返回值,赋值给data,如果有新值,则触发watch。
总结一下:
一个简单的storage,愣是玩出了花。
虽然看上去很简单,但是内部做了大量的处理以及工作。
有几个点,我觉得是比较实用的,一个是wtach对源数据的的监听,一个是对数据的序列化处理,可以避免很多格式转化的工作。
今天的文章就到这里了,如果看完觉得有收获,欢迎点赞+关注。
转载自:https://juejin.cn/post/7237053697528135736