本文不会放api的用法,建议先看看是怎么用的
项目架构 采用monorepo的形式,项目目录下有多个子项目,下面放了资料链接和几处用法,其他本文不多赘述。
现代前端工程为什么越来越离不开 Monorepo? 为什么使用pnpm可以光速建立好用的monorepo(比yarn/lerna效率高) pnpm workspace文档
为了使所有子项目都使用同一个依赖版本,使用pnpm.overrides 配置,如下
1 2 3 4 5 6 7 8 { "pnpm" : { "overrides" : { "vue-demi" : "0.12.1" , "vite" : "^2.6.7" } } }
在正常配置下,如果直接pnpm install
将会在每个子项目的node_modules
有同样的依赖,但是所有子项目相同的依赖其实只需要提取出来放在根目录的node_modules
下即可,可配置如下
1 2 3 4 "peerDependencies" : { "@vue/composition-api" : "^1.1.0" , "vue" : "^2.6.0 || ^3.2.0" },
详情请看如下链接
探讨npm依赖管理之peerDependencies pnpm monorepo之多组件实例和peerDependencies困境回溯 如何处理 peers
前置知识 unref - ref的反操作
1 2 3 4 function unref <T >(ref: T | Ref<T> ): T { return isRef(ref) ? ref.value : ref }
使用例子:实现一个响应式的add函数,可以传入ref或值
1 2 3 4 5 6 function add ( a: Ref<number > | number , b: Ref<number > | number ) { return computed(() => unref(a) + unref(b)) }
MaybeRef类型 vueuse大量使用MaybeRef
来支持可选择性的响应式参数
1 type MaybeRef<T> = Ref<T> | T
上面的加法函数就可以简写为
1 2 3 function add (a: MaybeRef<number >, b: MaybeRef<number > ) { return computed(() => unref(a) + unref(b)) }
Effect作用域API Vue官方文档:Effect 作用域 API
动机
在Vue的setup中,响应式effect会在初始化的时候被收集,在实例被卸载的时候,响应式effect就会自动被取消了,但是在我们在组件外写一个独立的包(就如vueuse)时,我们该如何停止computed & watch的响应式依赖呢?vue3.2提出了effectScope
effectScope 利用effectScope
创建一个作用域对象,如下面接口定义所示,run
接受一个函数,这个作用域对象会自动捕获函数内部的响应式effect (例如计算属性或侦听器)
类型
1 2 3 4 5 6 function effectScope (detached?: boolean ): EffectScope interface EffectScope { run<T>(fn: () => T): T | undefined stop(): void }
示例
1 2 3 4 5 6 7 8 9 10 const scope = effectScope()scope.run(() => { const doubled = computed(() => counter.value * 2 ) watch(doubled, () => console .log(doubled.value)) watchEffect(() => console .log('Count: ' , doubled.value)) }) scope.stop()
getCurrentScope 如果有,则返回当前活跃的 effect 作用域 。
类型
1 function getCurrentScope ( ): EffectScope | undefined
onScopeDispose 在当前活跃的 effect 作用域 上注册一个处理回调。该回调会在相关的 effect 作用域结束之后被调用,在VCA函数中可用作onUnmounted
的非组件替代品,区别在于其工作在scope中而不是组件实例
1 2 3 onScopeDispose(() => { window .removeEventListener('mousemove' , handler) })
示例
在vue的rfc中reactivity-effect-scope 有这样的一个例子,如果有个监控鼠标位置的hook(useMouse),需要监听mousemove
事件,如果在多个组件调用了这个hook,而内部是通过在onUnmounted
钩子来移除mousemove
监听器,onUnmounted
耦合在每个组件实例,则无法以更有效率的方式共享这个mousemove
监听器
为了做到在组件间共享useMouse
的响应式effect和监听器,可以创建一个函数来管理scope如下(这个hook也在vueuse中)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 function createSharedComposable (composable ) { let subscribers = 0 let state, scope const dispose = () => { if (scope && --subscribers <= 0 ) { scope.stop() state = scope = null } } return (...args ) => { subscribers++ if (!state) { scope = effectScope(true ) state = scope.run(() => composable(...args)) } onScopeDispose(dispose) return state } }
可以看到dispose函数的含义是,当没有一个组件在使用 它的时候会注销(dispose)创建的effectScope
,我们只需要如下操作,即可得到一个在所有组件共享 的useMouse
1 const useSharedMouse = createSharedComposable(useMouse)
更加详细的关于effectScope
的讨论在Reactivity’s effectScope
API #212
本来是打算完全按照官网的分类进行解读,但是由于VCA的思想,hook被拆的很碎再组合在一起,并不能单纯靠分类进行解读,所以在讲某个分类的hook时也会带上其他的hooks,接下来是对我觉得 常用 或者 有学习到东西 的hook源码进行解读
Browser
浏览器相关的hook,基于web暴露的api来实现
可配置全局对象 Browser hook的源码开头都有或类似下面这一段
1 2 3 4 5 6 7 export const defaultWindow = isClient ? window : undefined export function useXXX <T >(options: ConfigurableWindow = {} ) { const { window = defaultWindow } = options window .xxx }
用户可以配置当前window对象,这种配置方式对于使用iframe 和测试环境 不同的window对象十分有用
useEventListener 先上一个简易版本的useEventListener
,利用VCA,将事件的监听和注销放在一个函数中处理
1 2 3 4 5 6 7 8 9 function useEventListener (target, listener, options, target = window ) { onMounted(() => { target.addEventListener(type, listener, options) }) onUnmounted(() => { target.removeEventListener(type, listener, options) }) }
除此之外,源码还提供了当target
的类型是ref
时的情况,为了减少篇幅,下面的代码删减了参数为空的边界情况
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 export function useEventListener (...args: any [] ) { let target: MaybeRef<EventTarget> | undefined = defaultWindow; let event: string let listener: any let options: any [target, event, listener, options] = args let cleanup = noop const stopWatch = watch( () => unref(target), (el ) => { cleanup() if (!el) return el.addEventListener(event, listener, options) cleanup = () => { el.removeEventListener(event, listener, options) cleanup = noop } }, { immediate : true , flush : 'post' }, ) const stop = () => { stopWatch() cleanup() } tryOnScopeDispose(stop) return stop }
为什么需要支持ref target 用unref
得到ref
中的dom元素,监听dom元素的改变以重新监听事件,应用场景就放官网例子
1 2 3 4 <template > <div v-if ="cond" ref ="element" > Div1</div > <div v-else ref ="element" > Div2</div > </template >
1 2 3 4 import { useEventListener } from '@vueuse/core' const element = ref<HTMLDivElement>()useEventListener(element, 'keydown' , (e ) => { console .log(e.key) })
为什么需要watch设置flush为post 详情请看#356 Bug: useEventListener doesn’t work correctly with v-if ,意思就是新版vue 的watch
默认在所有组件update前执行,那么ref没有更新导致事件没有更新,所以需要在组件update完毕后更新事件。
tryOnScopeDispose是啥 先判断是否有活跃的effectScope,有的话用onScopeDispose
在其上注册fn回调
1 2 3 4 5 6 7 export function tryOnScopeDispose (fn: Fn ) { if (getCurrentScope()) { onScopeDispose(fn) return true } return false }
useEyeDropper new window.EyeDropper()
可以获取取色器 的功能
window.matchMedia(query)
返回MediaQueryList
类型
1 2 3 4 5 6 7 8 interface MediaQueryList extends EventTarget { readonly matches: boolean ; readonly media: string ; onchange: ((this : MediaQueryList, ev: MediaQueryListEvent ) => any ) | null ; } let mql: MediaQueryList = window .matchMedia('(max-width: 600px)' );
usePermission navigator.permissions 可以获取用户权限(摄像头、麦克风等)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 const isSupported = Boolean (navigator && 'permissions' in navigator)const state = ref<PermissionState | undefined >() const onChange = () => { if (permissionStatus) state.value = permissionStatus.state } try { permissionStatus = await navigator!.permissions.query({ name : 'camera' }) useEventListener(permissionStatus, 'change' , onChange) onChange() } catch { state.value = 'prompt' }
usePreferredLanguages navigator.languages 可以获取用户的偏好语言,可以通过languagechange
事件监听改变
1 2 3 4 5 const value = ref<readonly string []>(navigator.languages)useEventListener(window , 'languagechange' , () => { value.value = navigator.languages })
useShare navigator.share 可以进行分享
1 2 3 4 5 6 7 8 9 10 11 12 13 const isSupported = navigator && 'canShare' in navigatorif (isSupported) { granted = navigator.canShare({ title?: string files?: File[] text?: string url?: string }) if (granted) return navigator.share!(data) }
useWakeLock Screen Wake Lock API 可以阻止设备变暗或锁屏
1 2 3 4 5 6 7 8 9 10 11 12 const isSupported = navigator && 'wakeLock' in navigatorfunction request (type : WakeLockType ) { if (!isSupported) return wakeLock = await navigator.wakeLock.request(type ) } function release ( ) { if (!isSupported || !wakeLock) return await wakeLock.release() wakeLock = null }
Watch State createGlobalState
用来做挂组件公共状态管理
简单的单例模式实现,effectScope
的参数为true时,其不会被父scope收集和回收,独立存在
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 export function createGlobalState <T >( stateFactory: () => T, ): CreateGlobalStateReturn <T > { let initialized = false let state: T const scope = effectScope(true ) return () => { if (!initialized) { state = scope.run(stateFactory)! initialized = true } return state } }
useLocalStorage 1 2 3 4 5 6 7 8 export function useLocalStorage <T extends (string |number |boolean |object |null )> ( key: string , initialValue: MaybeRef<T>, options: StorageOptions<T> = {}, ): RemovableRef <any > { const { window = defaultWindow } = options return useStorage(key, initialValue, window ?.localStorage, options) }
可以看到就是用useStorage
来实现的,useSessionStorage
也是一样,下面我们来看看useStorage
useStorage 在浏览器默认的Storage之上像store.js 一样对数据进行预处理(序列化),否则如果存一个对象在浏览器的Storage存的会是xxx.toString()
之后的值。但是useStorage
比起store.js 更进一步的增加了对Map
和Set
类型的数据的处理(Map
和Set
存在默认的Storage和store.js都是空对象 )
利用适配器模式,对每种数据类型定义read
、write
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 export const StorageSerializers: Record<'boolean' | 'object' | 'number' | 'any' | 'string' | 'map' | 'set' , 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 >).entries())), }, }
接下来判断用户传入的数据类型,进行选择对应的read
、write
方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 const type = rawInit == null ? 'any' : rawInit instanceof Set ? 'set' : rawInit instanceof Map ? 'map' : typeof rawInit === 'boolean' ? 'boolean' : typeof rawInit === 'string' ? 'string' : typeof rawInit === 'object' ? 'object' : Array .isArray(rawInit) ? 'object' : !Number .isNaN(rawInit) ? 'number' : 'any' const serializer = options.serializer ?? StorageSerializers[type ]
下面是对数据的初始化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 const rawInit: T = unref(initialValue) const data = (shallow ? shallowRef : ref)(initialValue) as Ref<T> if (!storage) return try { const rawValue = storage.getItem(key) if (rawValue == null ) { data.value = rawInit if (writeDefaults && rawInit !== null ) storage.setItem(key, serializer.write(rawInit)) } else { data.value = serializer.read(rawValue) } } catch (e) { onError(e) }
然后是根据监听data的变化存入Storage,watchWithFilter
可以看做是新增了eventFilter
选项的watch
,在后文会描述,也是vueuse很重要的一个特性
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 watchWithFilter( data, () => { try { if (data.value == null ) storage.removeItem(key) else storage.setItem(key, serializer.write(data.value)) } catch (e) { onError(e) } }, { flush, deep, eventFilter, }, )
但仍有巨隐蔽的bug(哈哈哈),为了支持响应式的存取Storage,用了ref类型的data,在同源下的多个标签的情况下,其中一个页面对另一个页面用useStorage
修改了数据,但另一个页面useStorage内部的data还是原来的值
解决方式:监听storage 事件(当页面使用的storage被其他页面修改时会触发)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 function read (event?: StorageEvent ) { if (!storage || (event && event.key !== key)) return try { const rawValue = event ? event.newValue : storage.getItem(key) } catch (e) { onError(e) } } if (window && listenToStorageChanges) useEventListener(window , 'storage' , e => setTimeout (() => read(e), 0 ))
Sensors onClickOutside
Animation useRafFn
可以暂停、恢复、获取当前状态的requestAnimationFrame
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 export function useRafFn (fn: Fn, options: RafFnOptions = {} ): Pausable { const { immediate = true , window = defaultWindow, } = options const isActive = ref(false ) function loop ( ) { if (!isActive.value) return fn() if (window ) window .requestAnimationFrame(loop) } function resume ( ) { if (!isActive.value) { isActive.value = true loop() } } function pause ( ) { isActive.value = false } if (immediate) resume() tryOnScopeDispose(pause) return { isActive, pause, resume, } }
留有疑问:为什么pause中不用cancelAnimationFrame
来取消requestAnimationFrame
useTransition 可配置项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 type CubicBezierPoints = [number , number , number , number ]type EasingFunction = (n: number ) => number export function useTransition ( source: Ref<number | number []> | MaybeRef<number >[], options: TransitionOptions = {}, ): ComputedRef <any > { const { delay = 0 , disabled = false , duration = 1000 , onFinished = noop, onStarted = noop, transition = linear, } = options }
预处理
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 export function useTransition ( source: Ref<number | number []> | MaybeRef<number >[], options: TransitionOptions = {}, ): ComputedRef <any > { const currentTransition = computed(() => { const t = unref(transition) return isFunction(t) ? t : createEasingFunction(t) }) const sourceValue = computed(() => { const s = unref<number | MaybeRef<number >[]>(source) return isNumber(s) ? s : s.map(unref) as number [] }) const sourceVector = computed(() => isNumber(sourceValue.value) ? [sourceValue.value] : sourceValue.value) } function createEasingFunction ([p0, p1, p2, p3]: CubicBezierPoints ): EasingFunction { const a = (a1: number , a2: number ) => 1 - 3 * a2 + 3 * a1 const b = (a1: number , a2: number ) => 3 * a2 - 6 * a1 const c = (a1: number ) => 3 * a1 const calcBezier = (t: number , a1: number , a2: number ) => ((a(a1, a2) * t + b(a1, a2)) * t + c(a1)) * t const getSlope = (t: number , a1: number , a2: number ) => 3 * a(a1, a2) * t * t + 2 * b(a1, a2) * t + c(a1) const getTforX = (x: number ) => { let aGuessT = x for (let i = 0 ; i < 4 ; ++i) { const currentSlope = getSlope(aGuessT, p0, p2) if (currentSlope === 0 ) return aGuessT const currentX = calcBezier(aGuessT, p0, p2) - x aGuessT -= currentX / currentSlope } return aGuessT } return (x: number ) => p0 === p1 && p2 === p3 ? x : calcBezier(getTforX(x), p1, p3) }
提供了几种过渡预设
1 2 3 4 5 export const TransitionPresets: Record<string , CubicBezierPoints | EasingFunction> = { easeInSine : [0.12 , 0 , 0.39 , 0 ], easeOutSine : [0.61 , 1 , 0.88 , 1 ], }
详情可以看 缓动函数 、 MDN:缓动函数
关键逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 const outputVector = ref(sourceVector.value.slice(0 )) const { resume, pause } = useRafFn(() => { const now = Date .now() const progress = clamp(1 - ((endAt - now) / currentDuration), 0 , 1 ) outputVector.value = startVector.map((val, i ) => val + ((diffVector[i] ?? 0 ) * currentTransition.value(progress))) if (progress >= 1 ) { pause() onFinished() } }, { immediate : false }) const timeout = useTimeoutFn(start, delay, { immediate : false }) const start = () => { pause() currentDuration = unref(duration) diffVector = outputVector.value.map((n, i ) => (sourceVector.value[i] ?? 0 ) - (outputVector.value[i] ?? 0 )) startVector = outputVector.value.slice(0 ) startAt = Date .now() endAt = startAt + currentDuration resume() onStarted() } watch(sourceVector, () => { if (unref(disabled)) { outputVector.value = sourceVector.value.slice(0 ) } else { if (unref(delay) <= 0 ) start() else timeout.start() } }, { deep : true }) return computed(() => { const targetVector = unref(disabled) ? sourceVector : outputVector return isNumber(sourceValue.value) ? targetVector.value[0 ] : targetVector.value })
未完待续。。。