我正在参加「启航方案」
准备工作
1. 准备环境
1.1 拉取源码
github下载 vueuse 源码,先看README.md
和contributing.md
奉献文档。源码克隆好,然后依据文档准备好项目运转调试的环境。
1.2 准备环境
依据contributing.md
奉献文档,该项目需求 pnpm 装置依靠/运转项目,而官网最近 pnpm
8.x 版别需求装置 node
v16.14+ 版别。
做好如上环境准备工作,就能够本地发动源码项目啦,发动指令如下:
# 装置依靠
pnpm install
# 本地发动(基于VitePress)
pnpm dev
1.3 debug调试
项目发动后,f12 打开浏览器开发者东西 – Sources
面板,查找需求调试的源码文件,增加 Breakpoints 断点,即可调试任意源代码。
2. 了解Vitepress
VitePress 是基于vite
的vuepress兄弟版,特别适合写博客网站、技能文档、面试题等。vueuse 的本地服务是借助 VitePress 发动的,组件库代码调试和技能文档检查也比较方便。
使用指南:《vitepress 中文文档 》
3. 了解单元测试结构Vitest
Vitest是基于Vite
单元测试结构,它的特色如下:
- 能重复使用
Vite
的配置、转换器、解析器和插件 - 开箱即用的
TypeScript
/JSX
支持等
Vitest vscode扩展运转/调试
装置vitest vscode 扩展、vitest-runner vscode 扩展 后,vscode编辑器左侧导航多了“测试”图标,点击进去挑选出项目下所有测试用例,即可运转/调试。
参考:听说90%的人不知道能够用测试用例(Vitest)调试开源项目(Vue3) 源码
源码学习
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 willNOTbe 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
也会愈加顺畅、轻松!