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?

相关推荐
智界工具库27 分钟前
【探索前端技术之 React Three.js—— 简单的人脸动捕与 3D 模型表情同步应用】
前端·javascript·react.js
独泪了无痕34 分钟前
研究 Day.js 及其在 Vue3 和 Vue 框架中的应用详解
前端·vue.js·element
努力搬砖的程序媛儿40 分钟前
uniapp悬浮可拖拽按钮
java·前端·uni-app
浪浪山小白兔1 小时前
HTML 表单和输入标签详解
前端·html
helianying551 小时前
AI赋能零售:ScriptEcho如何提升效率,优化用户体验
前端·人工智能·ux·零售
Nickyang2 小时前
如何用Trae打造一键登录神器?Chrome插件开发实战
前端·javascript·trae
逆旅行天涯2 小时前
【vitePress】基于github快速添加评论功能(giscus)
前端·github
我有一棵树2 小时前
style标签没有写lang=“scss“引发的 bug 和反思
前端·bug·scss
陈奕迅本讯3 小时前
HTML5和CSS3拔高
前端·css3·html5