读 vueuse 源码,自己封装 vue hook 的思路拓宽了
准备工作
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 断点,即可调试任意源代码。

2. 了解Vitepress
VitePress 是基于vite的vuepress兄弟版,特别适合写博客网站、技术文档、面试题等。vueuse 的本地服务是借助 VitePress 启动的,组件库代码调试和技术文档查看也比较方便。
3. 了解单元测试框架Vitest
Vitest 是基于 Vite 单元测试框架,它的特点如下:
- 能重复使用 Vite的配置、转换器、解析器和插件
- 开箱即用的 TypeScript/JSX支持等
Vitest vscode扩展运行/调试


源码学习
vueuse 核心组件库封装了 State、Elements、Browser、Sensors、Network、Animation、Component、Watch、Reactivity、Array、Time、Utilities 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.onfocus和window.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 也会更加顺畅、轻松!
转载自:https://juejin.cn/post/7227672630941351994




