1. 进入视口时的回调onEnter)。

  2. 离开视口时的回调onLeave)。

指令代码

// src/directives/viewport.ts
import type { App, Directive, DirectiveBinding } from 'vue'

// 定义回调函数类型
type ViewportCallback = (entry: IntersectionObserverEntry) => void

// 定义指令配置选项接口
interface ViewportOptions {
  threshold?: number | number[] // 触发回调的可见比例阈值
  root?: Element | null        // 观察器的根元素
  rootMargin?: string         // 根元素的边距
  once?: boolean              // 是否只触发一次
}

// 扩展 HTMLElement,添加自定义属性
interface ViewportElement extends HTMLElement {
  _observer?: IntersectionObserver
  _options?: ViewportOptions
  _onEnterCallback?: ViewportCallback
  _onLeaveCallback?: ViewportCallback
}

// 默认配置
const DEFAULT_OPTIONS: ViewportOptions = {
  threshold: 0.1,      // 默认当元素10%可见时触发
  root: null,          // 默认使用浏览器视口
  rootMargin: '0px',   // 默认无边距
  once: false          // 默认持续观察
}

// 创建指令
export const vViewport: Directive<ViewportElement, [ViewportCallback, ViewportCallback?, ViewportOptions?]> = {
  mounted(el: ViewportElement, binding: DirectiveBinding) {
    // 解析回调函数和配置选项
    const onEnter = binding.value[0]
    const onLeave = binding.value[1] || (() => {}) // 如果没有提供离开回调,使用空函数
    const userOptions = binding.value[2] || {}
    
    // 合并配置选项
    const options = { ...DEFAULT_OPTIONS, ...userOptions }
    
    // 存储回调和选项,用于更新时比较
    el._onEnterCallback = onEnter
    el._onLeaveCallback = onLeave
    el._options = options

    // 创建观察器实例
    const observer = new IntersectionObserver((entries: IntersectionObserverEntry[]) => {
      entries.forEach((entry: IntersectionObserverEntry) => {
        if (entry.isIntersecting) {
          // 进入视口时触发 onEnter 回调
          onEnter(entry)

          // 如果设置了 once 选项,在首次触发后取消观察
          if (options.once) {
            observer.unobserve(el)
          }
        } else {
          // 离开视口时触发 onLeave 回调
          onLeave(entry)
        }
      })
    }, {
      threshold: options.threshold,
      root: options.root,
      rootMargin: options.rootMargin
    })

    // 开始观察元素
    observer.observe(el)
    el._observer = observer
  },

  // 更新时进行配置比较
  updated(el: ViewportElement, binding: DirectiveBinding) {
    const newOnEnter = binding.value[0]
    const newOnLeave = binding.value[1] || (() => {})
    const newOptions = binding.value[2] || {}
    
    // 检查回调或配置是否发生变化
    const optionsChanged = JSON.stringify(newOptions) !== JSON.stringify(el._options)
    const onEnterChanged = newOnEnter !== el._onEnterCallback
    const onLeaveChanged = newOnLeave !== el._onLeaveCallback

    // 如果有变化,重新创建观察器
    if (optionsChanged || onEnterChanged || onLeaveChanged) {
      // 清理旧的观察器
      el._observer?.disconnect()
      
      // 重新挂载指令
      vViewport.mounted!(el, binding)
    }
  },

  // 组件卸载时清理资源
  unmounted(el: ViewportElement) {
    if (el._observer) {
      el._observer.disconnect()
      delete el._observer
      delete el._onEnterCallback
      delete el._onLeaveCallback
      delete el._options
    }
  }
}

// 注册指令的函数
export const registerViewportDirective = (app: App): void => {
  app.directive('viewport', vViewport)
}

// 默认导出注册函数
export default registerViewportDirective

使用方法

1. 注册指令

main.ts 中注册指令:

// main.ts
import { createApp } from 'vue'
import App from './App.vue'
import registerViewportDirective from './directives/viewport'

const app = createApp(App)
registerViewportDirective(app)
app.mount('#app')

2. 在组件中使用

基本用法:监听进入和离开视口

<template>
  <div 
    v-viewport="[onEnterViewport, onLeaveViewport]" 
    class="target-element"
  >
    监听进入和离开视口
  </div>
</template>

<script setup lang="ts">
// 定义进入视口的回调
const onEnterViewport = (entry: IntersectionObserverEntry) => {
  console.log('元素进入视口', entry.target)
  entry.target.classList.add('visible')
}

// 定义离开视口的回调
const onLeaveViewport = (entry: IntersectionObserverEntry) => {
  console.log('元素离开视口', entry.target)
  entry.target.classList.remove('visible')
}
</script>

<style scoped>
.target-element {
  min-height: 200px;
  background: #f0f0f0;
  margin: 20px 0;
  transition: opacity 0.3s ease;
  opacity: 0;
}

.target-element.visible {
  opacity: 1;
}
</style>

配置选项:自定义阈值和行为

<template>
  <div 
    v-viewport="[onEnterViewport, onLeaveViewport, { threshold: 0.5, rootMargin: '50px', once: false }]" 
    class="target-element"
  >
    配置选项示例
  </div>
</template>

<script setup lang="ts">
// 定义进入视口的回调
const onEnterViewport = (entry: IntersectionObserverEntry) => {
  console.log('元素进入视口,50% 可见时触发')
  entry.target.classList.add('visible')
}

// 定义离开视口的回调
const onLeaveViewport = (entry: IntersectionObserverEntry) => {
  console.log('元素离开视口')
  entry.target.classList.remove('visible')
}
</script>

配置选项说明

v-viewport 指令支持以下配置选项:

配置项

类型

默认值

说明

threshold

`number

number[]`

0.1

root

`Element

null`

null

rootMargin

string

'0px'

根元素的边距,类似 CSS margin。

once

boolean

false

是否只触发一次。如果为 true,首次触发后会自动停止观察。


示例效果

  1. 进入视口时:元素会添加 visible 类,触发动画或其他效果。

  2. 离开视口时:元素会移除 visible 类,恢复初始状态。


总结

通过扩展 v-viewport 指令,我们不仅可以监听元素进入视口,还可以监听元素离开视口。这使得指令更加灵活,适用于更多场景,例如:

  • 动画的触发和回退。

  • 曝光统计和离开统计。

  • 动态加载和卸载内容。

希望本文对你有所帮助!如果你有任何问题或建议,欢迎留言交流 😊