1.4 State -- useStorage

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

// bind number
const count = useStorage('my-count', 0) // returns Ref

// bind string with SessionStorage
const id = useStorage('my-id', 'some-string-id', sessionStorage) // returns Ref

// 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

要搞清楚源码的实现,只需要搞懂一下几种场景:

  1. 初始化,包括合并默认项是如何做的。
  2. 取值逻辑
  3. 设置值的逻辑,主要是如何通过设置值,响应式的修改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)
    }
  }
}

获取类型的代码如下,可以看到就是基本的instanceoftypeof方法

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 = kkstorage中存储的值会发生改变,也就是说statestorage中的值都变了。

但是这个时候,state2并不知道storage中的值发生了变化,因为statestate2在同一个页面,storage事件不会触发。所以需要手动派发事件,让state2也响应式的改变。

问题二:在update方法中,会执行data.value = read(event)。也就是说:data的变化,引发了更新,更新又再次修改了data的值,这是为什么呢?那为什么在update方法中需要执行read方法呢?

原因是state和state2可能会有不同的序列化函数,执行read方法,就可以让同一份数据以不同的反序列化方式被解析。

你可能感兴趣的:(vueuse源码解析,javascript,开发语言,ecmascript)