1. 前言
你应该具备的知识
1. TypeScript基础知识(对这篇文章并不是很重要,但后面会说关于类型的问题)
2. JavaScript基础知识
3. Vue3的基础知识
什么是Vueuse
官网是这样介绍的
Collection of essential Vue Composition Utilities
通俗的翻译过来就是
基于Vue Compistion API的一些基本函数库
这个库目前是我在Vue项目中经常使用的库,不过周下载量并不是很高,看来宣传的力度还是不强呀
对比React的hook库
这个库很有意思,有各种奇奇怪怪的钩子,甚至有检测你设配当前电池状态的钩子,我当前的设备就在充电,它给我检测出来了
还能监听你的内存状态
不过我的内存是16GB,为什么到这就是4GB了,难道是监听的当前网页的内存么?
作者信息
这个库是由Vue核心成员Anthony Fu发起的
我个人是很崇拜这位大神的,尤其是他那满满的绿点和commit的次数,无不震撼着我这幼小的心灵
如何使用
举个例子吧,比如说你当前想获取用户鼠标的坐标时,就可以用到一个hook -- useMouse
import { useMouse } from '@vueuse/core'
const { x, y, sourceType } = useMouse()
而其中的类型定义如下
export declare type MouseSourceType = "mouse" | "touch" | null
export declare function useMouse(options?: MouseOptions): {
x: Ref<number>
y: Ref<number>
sourceType: Ref<MouseSourceType>
}
可以发现x
y
的类型都是Ref
,所以我们使用的时候需要.value
一下
什么是effectScope
这是官网的介绍
Effect 作用域是一个高阶的 API,主要服务于库作者。关于其使用细节请咨询相应的 RFC
创建一个 effect 作用域对象,以捕获在其内部创建的响应式 effect (例如计算属性或侦听器),使得这些 effect 可以一起被处理。
类型:
function effectScope(detached?: boolean): EffectScope
interface EffectScope {
run<T>(fn: () => T): T | undefined // 如果这个域不活跃则为 undefined
stop(): void
}
示例:
const scope = effectScope()
scope.run(() => {
const doubled = computed(() => counter.value * 2)
watch(doubled, () => console.log(doubled.value))
watchEffect(() => console.log('Count: ', doubled.value))
})
// 处理该作用域内的所有 effect
scope.stop()
#getCurrentScope
如果有,则返回当前活跃的 effect 作用域。
类型:
function getCurrentScope(): EffectScope | undefined
onScopeDispose
在当前活跃的 effect 作用域上注册一个处理回调。该回调会在相关的 effect 作用域结束之后被调用。
该方法在复用组合式函数时可用作 onUnmounted
的非组件耦合替代品,因为每个 Vue 组件的 setup()
函数也同样在 effect 作用域内被调用。
我推荐大家去看对应的RFC以此来了解更多的信息
下面我介绍一下RFC中所说的effectScope的动机
In Vue's component
setup()
, effects will be collected and bound to the current instance. When the instance get unmounted, effects will be disposed automatically. This is a convenient and intuitive feature.
However, when we are using them outside of components or as a standalone package, it's not that simple. For example, this might be what we need to do for disposing the effects ofcomputed
&watch
大意就是说
在Vue组件中我们可以用类似于onUnmounted这种生命周期钩子来做一些在组件销毁时我们想做的事情,但当我们想在组件之外来做时,就不是那么容易了,比如你想在组件外实现一个钩子,在组件销毁时去做一些事情,这时你用unMounted就会出现一些问题,这时我们就需要effectScope的帮助了
为什么需要effectScope
举个RFC中的例子 假设你现在需要得到用户鼠标的坐标,你是这样写的
function useMouse() {
const x = ref(0)
const y = ref(0)
function handler(e) {
x.value = e.x
y.value = e.y
}
window.addEventListener('mousemove', handler)
onUnmounted(() => {
window.removeEventListener('mousemove', handler)
})
return { x, y }
}
如果你像这样去实现useMouse
,那么你在不同的组件中使用useMouse
的时候,每个组件都会创建和重新计算x
y
,如果你有大量的组件都需要使用useMouse
,那就会增加性能开销,这并不好,所以这时我们需要effectScope
下面看看effectScope
是如何实现的
替换onUnmounted
为onScopeDispose
onUnmounted(() => {
window.removeEventListener('mousemove', handler)
})
// 改为
onScopeDispose(() => {
window.removeEventListener('mousemove', handler)
})
创建一个函数去管理useMouse的使用
使用闭包的原理来管理状态,以此来确保x
与y
只计算一次
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
}
}
现在我们可以使用useMouse
像这样
const useSharedMouse = createSharedComposable(useMouse)
现在,我们就能拿到对应的x, y了
我对这个例子做了一个小demo,大家可以点开看看
证明useSharedMouse
最后雀氏只会计算一次,大大的提高性能,防止眼神有些快的小伙伴错过链接,我在这补上demo的链接
stackblitz.com/edit/vue-jk…
effectScope在Vueuse中的使用
useEventListener
useEventListener的作用
以我们熟悉的useMouse
为例子,在源码中,并不是使用
window.addEventListener('mousemove', handler)
来直接监听事件的,而是使用
useEventListener(window, 'mousemove', mouseHandler, { passive: true })
明显可以猜出这个钩子是拿来监听事件的,并且在组件销毁后,自动注销事件,避免内存的泄漏,下面我们来探究useEventListener
的实现
源码探究
React的hook可以在组件销毁时自动执行hook返回出的函数,比如这样
const useMouse = () => {
const [pos, setPos] = useState({ x: 0, y: 0 })
const handler = (e) => {
setPos({
x: e.x,
y: e.y
})
}
window.addEventListener('mousemove', handler)
const destroyEventListenr = () => {
widnow.removeEventListener('mousemove', handler)
}
return () => {
destroyEventListenr()
}
}
这样在组件销毁的时候,会自动调用
() => {
destroyEventListenr()
}
这就是React Hook特性
那么如何在Vue中实现呢?先不管它怎么实现,我们先来研究一下Vueuse中useEventListener
是如何实现的
interface InferEventTarget<Events> {
addEventListener(event: Events, fn?: any, options?: any): any
removeEventListener(event: Events, fn?: any, options?: any): any
}
export function useEventListener(...args: any[]) {
let target: MaybeRef<EventTarget> | undefined
let event: string
let listener: any
let options: any
if (isString(args[0])) {
[event, listener, options] = args
target = defaultWindow
}
else {
[target, event, listener, options] = args
}
if (!target)
return noop
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
}
具体逻辑不作过多解释,我解释一下其中的MaybeRef
类型
export type MaybeRef<T> = T | Ref<T>
React Hook特性的实现
在useEventListener
的源码中,它是这样实现React Hook特性的
const stop = () => {
stopWatch()
cleanup()
}
tryOnScopeDispose(stop)
stop()
看字面意思就能猜出是注销事件的,重点在于tryOnScopeDispose
下面分析一下tryOnScopeDispose的源码
export function tryOnScopeDispose(fn: Fn) {
if (getCurrentScope()) {
onScopeDispose(fn)
return true
}
return false
}
其中getCurrentScope
说白了就是判断当前是否存在effectScope作用域,也就是你是不是在组件内部操作
比如你在main.js中操作
import { getCurrentScope } from 'vue'
console.log(getCurrentScope()) // undefined
而你在一个SFC文件中操作
import { getCurrentScope } from 'vue';
export default {
setup() {
const { state, computedTime } = useSharedMouse();
console.log(getCurrentScope());
}
}
输出的就是
所以tryOnScopeDispose
就是判断你当前的代码是不是在一个SFC文件内执行的,是的话那就执行onScopeDispose
,而onScopeDispose
,上面介绍了,是类似于onUnmounted
的操作
所以,useEventListener
利用Vue的特性实现了React Hook的特性
不过有趣的是,vueuse这个库的作者也是effectScope
这个SFC的作者,可以做一个猜想,Anthony Fu在做Vueuse这个库的时候,遇到了如何实现Hook特性的这个问题,因这个问题提出了effectScope
这个概念
写在最后
读大佬的源码是一件很美的事情,不但能提高自身的编码能力,还能碰见许多有意思的问题
比如,在Vueuse中,有一个公共模块名为shared
文章中的tryOnScopeDispose
就在这个模块内
要是我一般就命名为common
,不过shared
这个词似乎更合适,因为在这个模块中,也有实现本文提到的createSharedComposable
好啦,完结撒花(^o^)/~,如果有什么疑问,欢迎大家来评论区讨论