【源码阅读】如何把Storage封装成你用不起的样子?

1,922 阅读4分钟

离大谱,一个简单的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

image.png

先来看一下入参,以及作用。

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对源数据的的监听,一个是对数据的序列化处理,可以避免很多格式转化的工作。

今天的文章就到这里了,如果看完觉得有收获,欢迎点赞+关注。