likes
comments
collection
share

读 vueuse 源码,自己封装 vue hook 的思路拓宽了

作者站长头像
站长
· 阅读数 1

准备工作

1. 准备环境

1.1 拉取源码

github下载 vueuse 源码,先看README.md 和 contributing.md 贡献文档。源码克隆好,然后根据文档准备好项目运行调试的环境。

1.2 准备环境

根据contributing.md 贡献文档,该项目需要 pnpm 安装依赖/运行项目,而官网最近 pnpm8.x 版本需要安装 node v16.14+ 版本。

做好如上环境准备工作,就可以本地启动源码项目啦,启动命令如下:

# 安装依赖
pnpm install

# 本地启动(基于VitePress)
pnpm dev

1.3 debug调试

项目启动后,f12 打开浏览器开发者工具 - Sources面板,搜索需要调试的源码文件,添加 Breakpoints 断点,即可调试任意源代码。 读 vueuse 源码,自己封装 vue hook 的思路拓宽了

2. 了解Vitepress

VitePress 是基于vitevuepress兄弟版,特别适合写博客网站、技术文档、面试题等。vueuse 的本地服务是借助 VitePress 启动的,组件库代码调试和技术文档查看也比较方便。

3. 了解单元测试框架Vitest

Vitest 是基于 Vite 单元测试框架,它的特点如下:

  • 能重复使用 Vite 的配置、转换器、解析器和插件
  • 开箱即用的 TypeScript / JSX 支持等

Vitest vscode扩展运行/调试

读 vueuse 源码,自己封装 vue hook 的思路拓宽了

读 vueuse 源码,自己封装 vue hook 的思路拓宽了

源码学习

vueuse 核心组件库封装了 StateElementsBrowserSensorsNetworkAnimationComponentWatchReactivityArrayTimeUtilities 12个类型的工具函数 hook。下面我们挑几个常用hook来学习下vueuse源码吧!

State

useStorage

useStorage 是将localStorage、sessionStorage 封装成一个hook函数,本地存储的数据为响应式数据。源码如下:

/**
 * Reactive LocalStorage/SessionStorage.
 *
 * @see https://vueuse.org/useStorage
 */
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) {
    try {
      if (v == null) {
        storage!.removeItem(key)
      }
      else {
        const serialized = serializer.write(v)
        const oldValue = storage!.getItem(key)
        if (oldValue !== serialized) {
          storage!.setItem(key, serialized)

          // send custom event to communicate within same page
          // importantly this should _not_ be a StorageEvent since those cannot
          // be constructed with a non-built-in storage area
          if (window) {
            window.dispatchEvent(new CustomEvent<StorageEventLike>(customStorageEventName, {
              detail: {
                key,
                oldValue,
                newValue: serialized,
                storageArea: storage!,
              },
            }))
          }
        }
      }
    }
    catch (e) {
      onError(e)
    }
  }

  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)
    }
  }

  function updateFromCustomEvent(event: CustomEvent<StorageEventLike>) {
    update(event.detail)
  }

  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()
    }
  }
}

Tip: When using with Nuxt 3, this functions will NOT be auto imported in favor of Nitro's built-in useStorage(). Use explicit import if you want to use the function from VueUse.

Elements

useWindowFocus

useWindowFocus函数是使用window.onfocuswindow.onblur响应式跟踪 window 焦点事件。

export function useWindowFocus({ window = defaultWindow }: ConfigurableWindow = {}): Ref<boolean> {
  if (!window)
    return ref(false)

  const focused = ref(window.document.hasFocus())
  // 监听 blur 事件
  useEventListener(window, 'blur', () => {
    focused.value = false
  })
  // 监听 focus 事件
  useEventListener(window, 'focus', () => {
    focused.value = true
  })

  return focused
}

Component

useVirtualList

useVirtualList 是用来轻松创建虚拟列表。虚拟列表(有时称为虚拟滚动器)允许您以高效的方式呈现大量项目。通过使用 wrapper 元素来模拟 container 的完整高度,它们只呈现可视区内列表项。

export function useVirtualList<T = any>(list: MaybeRef<T[]>, options: UseVirtualListOptions): UseVirtualListReturn<T> {
  const { containerStyle, wrapperProps, scrollTo, calculateRange, currentList, containerRef } = 'itemHeight' in options
    ? useVerticalVirtualList(options, list)
    : useHorizontalVirtualList(options, list)

  return {
    list: currentList,
    scrollTo,
    containerProps: {
      ref: containerRef,
      onScroll: () => {
        calculateRange()
      },
      style: containerStyle,
    },
    wrapperProps,
  }
}

// 虚拟列表的实现
function useVerticalVirtualList<T>(options: UseVerticalVirtualListOptions, list: MaybeRef<T[]>) {
  const resources = useVirtualListResources(list)

  const { state, source, currentList, size, containerRef } = resources

  const containerStyle: StyleValue = { overflowY: 'auto' }

  const { itemHeight, overscan = 5 } = options

  const getViewCapacity = createGetViewCapacity(state, source, itemHeight)

  const getOffset = createGetOffset(source, itemHeight)

  const calculateRange = createCalculateRange('vertical', overscan, getOffset, getViewCapacity, resources)

  const getDistanceTop = createGetDistance(itemHeight, source)

  const offsetTop = computed(() => getDistanceTop(state.value.start))

  const totalHeight = createComputedTotalSize(itemHeight, source)

  useWatchForSizes(size, list, calculateRange)

  const scrollTo = createScrollTo('vertical', calculateRange, getDistanceTop, containerRef)

  const wrapperProps = computed(() => {
    return {
      style: {
        width: '100%',
        height: `${totalHeight.value - offsetTop.value}px`,
        marginTop: `${offsetTop.value}px`,
      },
    }
  })

  return {
    calculateRange,
    scrollTo,
    containerStyle,
    wrapperProps,
    currentList,
    containerRef,
  }
}

总结

以上行文,只是分享 vueuse 的调试技巧和对常用的 hook 源码进行简单分析,感兴趣的小伙伴可以继续阅读 vueuse 其他 hook 源码,相信读源码或多或少能够拓宽封装hook函数的思路,自己封装 vue hook 也会更加顺畅、轻松!