4.1 Sensors -- onClickOutside

4.1 Sensors -- onClickOutside

https://vueuse.org/onClickOutside

作用

监听当前的点击是否是在目标元素之外。在弹窗和下拉框中非常有用。

官方示例

html 复制代码
<template>
  <div ref="target">
    Hello world
  </div>
  <div>
    Outside element
  </div>
</template>

<script>
import { ref } from 'vue'
import { onClickOutside } from '@vueuse/core'

export default {
  setup() {
    const target = ref(null)
    onClickOutside(target, (event) => console.log(event))
    return { target }
  }
}
</script>
  • 无渲染组件的代码如下,通过@trigger触发回调
html 复制代码
<OnClickOutside @trigger="count++" :options="{ ignore: [/* ... */] }">
  <div>
    Click Outside of Me
  </div>
</OnClickOutside>
  • 指令用法,指令放在被监控元素上
html 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import { vOnClickOutside } from '@vueuse/components'

const modal = ref(false)
function closeModal() {
  modal.value = false
}
</script>

<template>
  <button @click="modal = true">
    Open Modal
  </button>
  <div v-if="modal" v-on-click-outside="closeModal">
    Hello World
  </div>
</template>

源码分析

我们自己实现的时候,一般都是判断鼠标点击的元素以及它的所有父元素,和目标元素是否有交集,通过递归el.parentNode实现。

看一下源码的实现。

  1. cleanup=[],其中的useEventListener返回的都是取消监听的函数。
  2. 监听了click、pointerdown、pointer事件,我们忽略iframe的情况。
  3. click事件始终触发handler回调,除非点击了忽略的元素。
  4. pointerdown判断鼠标按下,修改shouldListen,判断起点是否在target之外。
  5. pointerup回调listener事件,判断终点是否在target之外,同时又shouldListen,所以是否触发回调是二者共同控制的,只有起点和终点都在元素外才可以触发。

⚠️:思考一些问题:

1、为啥注册click、pointerdown、pointerup这么多事件?

​ 如果是鼠标左键点击: pointerdown --> pointerup --> click,如果是鼠标右键点击: pointerdown --> pointerup ,如果是拖动,也不会触发click,所以只监听click是不够的。

2、回调会重复触发吗?

​ 不会,因为pointerup的回调是fallback = window.setTimeout(() => listener(e), 50),所以如果触发了click,会执行window.clearTimeout(fallback)。所以只有click真正触发了回调。

3、右键点击是不是也会触发回调?

​ 从以上的结论来看,右键不会触发click,同时pointerup又限制了e.button === 0,只有左键起作用,所以右键不会触发回调。

typescript 复制代码
export function onClickOutside<T extends OnClickOutsideOptions>(
  target: MaybeElementRef,
  handler: OnClickOutsideHandler<{ detectIframe: T['detectIframe'] }>,
  options: T = {} as T,
) {
  const { window = defaultWindow, ignore = [], capture = true, detectIframe = false } = options

  if (!window)
    return

  let shouldListen = true

  let fallback: number

  /**
  * 判断当前这次事件是否应该被忽略
  */
  const shouldIgnore = (event: PointerEvent) => {
    // 如果用户传递ingore数组中有一项满足条件,这个就返回true
    return ignore.some((target) => {
      // 如果传的是字符串,就找到所有符合条件的dom元素。然后一项项进行比较。
      // 假如其中有一个dom满足:这个dom就是点击的dom,或者这个dom在被点击dom的冒泡路径上(也就是这个dom被鼠标冒泡路径上某一个dom包含)
      if (typeof target === 'string') {
        return Array.from(window.document.querySelectorAll(target))
          .some(el => el === event.target || event.composedPath().includes(el))
      }
      else {
        // 如果传递的是dom代理,直接比较
        const el = unrefElement(target)
        return el && (event.target === el || event.composedPath().includes(el))
      }
    })
  }

  const listener = (event: PointerEvent) => {
    window.clearTimeout(fallback)

    const el = unrefElement(target)
		// 如果点击了target元素或者target的子元素
    if (!el || el === event.target || event.composedPath().includes(el))
      return
    
		// 对于click事件,detail就是1;对于mousedown和mouseup,detail每次都+1;其他事件都是0
    if (event.detail === 0)
      shouldListen = !shouldIgnore(event)

    if (!shouldListen) {
      shouldListen = true
      return
    }
		
    // 点击了外部元素,且本次事件不被忽略,则触发用户传递的回掉
    handler(event)
  }

  const cleanup = [
    useEventListener(window, 'click', listener, { passive: true, capture }),
    
    useEventListener(window, 'pointerdown', (e) => {
      const el = unrefElement(target)
      if (el)
        // 按下时,鼠标不在target之内,则shouldListen=true
        shouldListen = !e.composedPath().includes(el) && !shouldIgnore(e)
    }, { passive: true }),
    
    useEventListener(window, 'pointerup', (e) => {
      
      // e.button === 0 表示鼠标左键
      if (e.button === 0) {
        const path = e.composedPath()
        e.composedPath = () => path
        fallback = window.setTimeout(() => listener(e), 50)
      }
    }, { passive: true }),
    
    detectIframe && useEventListener(window, 'blur', (event) => {
      const el = unrefElement(target)
      if (
        window.document.activeElement?.tagName === 'IFRAME'
        && !el?.contains(window.document.activeElement)
      )
        handler(event as any)
    }),
  ].filter(Boolean) as Fn[]

  const stop = () => cleanup.forEach(fn => fn())

  return stop
}
相关推荐
程序员黑豆9 小时前
Java类型推断完全指南:从var到菱形运算符,掌握使用限制与最佳实践
java·前端·ai编程
To_OC10 小时前
踩了个 TS 的坑之后,我终于把 type 和 interface 掰明白了
前端·react.js·typescript
GreenTea10 小时前
深度解读 Anthropic 多智能体报告:更强的模型 ≠ 更好的协调
前端·后端·算法
浮生望10 小时前
前端API工程化:用 Mock 数据与 Axios 配置实现独立于后端的并行开发
前端
万少11 小时前
给 DeepSeek Harness 装个"应用商店":一条命令,595 个插件随你逛
前端·javascript·后端
波波00711 小时前
C# 15 重磅新特性: 带标签 break 与 continue:重新定义嵌套循环控制流
服务器·前端·c#
Brown.alexis12 小时前
es6知识点1-自备使用
前端·ecmascript·es6
小爬的老粉丝13 小时前
JavaScript 纯前端预览 WPS:先识别容器,再路由解析器
前端·javascript·wps
计算机魔术师13 小时前
新兴多智能体系统的模式与问题
前端
用户0595401744614 小时前
AI Agent 上下文污染踩坑实录:用 pytest + Redis 揪出 3 类记忆串号 bug,排查 6 小时
前端·css