useEventListener - VueUse 源码解读

1、作用

轻松使用EventListener,在挂载时使用addEventListener注册,在卸载时自动删除EventListener。

2、使用案例

未使用

我们在做事件监听时,往往需要手动注册,并且在组件卸载时,删除事件; 例如:监听页面聚焦时更新数据

js 复制代码
const visibilitychangeFn = () => {
  // document.hidden为false表示视图页面进入了前台
  const pageVisibility = document.visibilityState;
  if (pageVisibility === 'visible') {
    init();
  }
};

// 监听页面聚焦
onMounted(() => {
  document.addEventListener('visibilitychange', visibilitychangeFn);
});

// 监听页面聚焦
onUnmounted(() => {
  document.removeEventListener('visibilitychange', visibilitychangeFn);
});

使用useEventListener

js 复制代码
const visibilitychangeFn = () => {
  // document.hidden为false表示视图页面进入了前台
  const pageVisibility = document.visibilityState;
  if (pageVisibility === 'visible') {
    console.log('page-visible, init')
  }
};

const stopListen = useEventListener('visibilitychange', visibilitychangeFn)

// 有需要时,你也可以手动取消监听
stopListen()

3、源码解读

1、使用多个函数重载实现支持多种参数 2、核心在于使用了 onScopeDispose 这个vue提供的api

这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的 vue官方文档 onScopeDispose

js 复制代码
import type { Arrayable, MaybeRefOrGetter, Fn, AnyFn, MaybeElementRef } from '../utils' 
import { noop, isObject, tryOnScopeDispose } from '../utils' 
import { watch } from 'vue'
import { unrefElement } from '../unrefElement'
import { toValue } from '../toValue'


interface InferEventTarget<Events> {
  addEventListener: (event: Events, fn?: any, options?: any) => any
  removeEventListener: (event: Events, fn?: any, options?: any) => any
}

export interface GeneralEventListener<E = Event> {
  (evt: E): void
}

/**
 * 重载1: 省略了,window目标
 */
export function useEventListener<E extends keyof WindowEventMap>(
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 2: 明确是 Window作为监听目标
 */
export function useEventListener<E extends keyof WindowEventMap>(
  target: Window,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: WindowEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 

/**
 * 重载 3: 明确是 Document作为监听目标
 */
export function useEventListener<E extends keyof DocumentEventMap>(
  target: DocumentOrShadowRoot,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: DocumentEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 4: 明确是 HTMLElement 作为监听目标
 */
export function useEventListener<E extends keyof HTMLElementEventMap>(
  target: MaybeRefOrGetter<HTMLElement | null | undefined>,
  event: Arrayable<E>,
  listener: Arrayable<(this: Window, ev: HTMLElementEventMap[E]) => any>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn 


/**
 * 重载 5: 自定义事件和监听目标(通过事件来推断target的类型)
 * EventType 是泛型,允许传入 任何 事件类型
 */
export function useEventListener<Names extends string, EventType = Event>(
  target: MaybeRefOrGetter<InferEventTarget<Names> | null | undefined>,
  event: Arrayable<Names>,
  listener: Arrayable<GeneralEventListener<EventType>>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn


/**
 * 重载 6: 自定义事件降级(不推断target的类型)
 */
export function useEventListener<EventType = Event>(
  target: MaybeRefOrGetter<EventTarget | null | undefined>,
  event: Arrayable<string>,
  listener: Arrayable<GeneralEventListener<EventType>>,
  options?: MaybeRefOrGetter<boolean | AddEventListenerOptions>
): Fn


export function useEventListener(...args: any[]) {
  let target: MaybeRefOrGetter<EventTarget> | undefined
  let events: Arrayable<string>
  let listeners: Arrayable<AnyFn>
  let options: MaybeRefOrGetter<boolean | AddEventListenerOptions> | undefined
  
  // 第一个参数是 事件类型
  if (typeof args[0] === 'string' || Array.isArray(args[0])) {
    [events, listeners, options] = args
    target = window
  }
  else { // 第一个参数是 监听目标
    [target, events, listeners, options] = args
  }

  // 无监听目标,无操作
  if(!target) return noop;

  // 兼容事件和响应是 数组的情况
  if (!Array.isArray(events))
    events = [events]
  if (!Array.isArray(listeners))
    listeners = [listeners]

  const cleanups: AnyFn[] = []
  const cleanup = () => {
    cleanups.forEach(fn => fn())
    cleanups.length = 0
  }

  // 注册时间监听
  const register = (el: any, event: string, listener: any, options: any) => {
    el.addEventListener(event, listener, options)
    return () => el.removeEventListener(event, listener, options)
  }


  const stopWatch = watch(
    ()=> [unrefElement(target as unknown as MaybeElementRef), toValue(options)],
    ([el, options]) => {
      cleanup()
      if (!el)
        return

      // 创建options的克隆,以避免在删除时被更改
      const optionsClone = isObject(options) ? { ...options } : options
      
      cleanups.push(
        ...(events as string[]).flatMap((event) => {
          return (listeners as AnyFn[]).map(listener => register(el, event, listener, optionsClone))
        })
      )
    }
  )

  const stop = () => {
    stopWatch()
    cleanup()
  }

  tryOnScopeDispose(stop)

  return stop
}

4、了解 effectScope、getCurrentScope、onScopeDispose

这些api,我们的日常开发基本上使用不上,做了解即可;

  • onScopeDispose

在当前活跃的 effect 作用域上注册一个处理回调函数。当相关的 effect 作用域停止时会调用这个回调函数。
这个方法可以作为可复用的组合式函数中 onUnmounted 的替代品,它并不与组件耦合,因为每一个 Vue 组件的 setup() 函数也是在一个 effect 作用域中调用的

这里截取,某乎上,算是大白话,容易理解的一个评论

如何理解Vue3里的EffectScope?

相关推荐
irving同学462385 分钟前
TypeORM 列装饰器完整总结
前端·后端·nestjs
彭于晏爱编程8 分钟前
你真的了解 Map、Set 嘛
前端
崔璨12 分钟前
详解Vue3的响应式系统
前端·vue.js
摸鱼的鱼lv12 分钟前
🔥 Vue.js组件通信全攻略:从父子传值到全局状态管理,一篇搞定所有场景!🚀
前端·vue.js
IT_陈寒24 分钟前
Java性能优化:10个让你的Spring Boot应用提速300%的隐藏技巧
前端·人工智能·后端
whysqwhw42 分钟前
Hippy 跨平台框架扩展原生自定义组件的完整实现方案对比
前端
dasseinzumtode1 小时前
nestJS 使用ExcelJS 实现数据的excel导出功能
前端·后端·node.js
子兮曰1 小时前
🔥C盘告急!WSL磁盘暴增?三招秒清20GB+空间
前端·windows·docker
Jinuss1 小时前
Vue3源码reactivity响应式篇之EffectScope
前端·vue3
stoneship1 小时前
网页截图API-Npm工具包分享
前端