我正在参加「启航方案」

准备工作

1. 准备环境

1.1 拉取源码

github下载 vueuse 源码,先看README.mdcontributing.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 是基于vite的vuepress兄弟版,特别适合写博客网站、技能文档、面试题等。vueuse 的本地服务是借助 VitePress 发动的,组件库代码调试和技能文档检查也比较方便。

使用指南:《vitepress 中文文档 》

3. 了解单元测试结构Vitest

Vitest是基于Vite单元测试结构,它的特色如下:

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

Vitest vscode扩展运转/调试

装置vitest vscode 扩展、vitest-runner vscode 扩展 后,vscode编辑器左侧导航多了“测试”图标,点击进去挑选出项目下所有测试用例,即可运转/调试。

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

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

参考:听说90%的人不知道能够用测试用例(Vitest)调试开源项目(Vue3) 源码

源码学习

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 willNOTbe auto imported in favor of Nitro’s built-inuseStorage(). 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 也会愈加顺畅、轻松!