2.5 Elements – useElementVisibility
https://vueuse.org/useElementVisibility
作用
追踪元素在视口中的可见性。
官方示例
<template>
<div ref="target">
<h1>Hello world</h1>
</div>
</template>
<script>
import { ref } from 'vue'
import { useElementVisibility } from '@vueuse/core'
export default {
setup() {
const target = ref(null)
const targetIsVisible = useElementVisibility(target)
return {
target,
targetIsVisible,
}
}
}
</script>
无渲染组件代码如下:
<UseElementVisibility v-slot="{ isVisible }">
Is Visible: {{ isVisible }}
</UseElementVisibility>
这个方法提供了指令性用法
<script setup lang="ts">
import { ref } from 'vue'
import { vElementVisibility } from '@vueuse/components'
const target = ref(null)
const isVisible = ref(false)
function onElementVisibility(state) {
isVisible.value = state
}
</script>
<template>
<div v-element-visibility="onElementVisibility">
{{ isVisible ? 'inside' : 'outside' }}
</div>
<!-- with options -->
<div ref="target">
<div v-element-visibility="[onElementVisibility, { scrollTarget: target }]">
{{ isVisible ? 'inside' : 'outside' }}
</div>
</div>
</template>
源码分析
实现此功能目前比较常用的API是IntersectionObserver
接口,提供了一种异步观察目标元素与其祖先元素或顶级文档视窗交叉状态的方法。
https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
但是此处没有使用这个API。
- 先看
hook
的代码,这也是使用方式最简单的
export function useElementVisibility(
element: MaybeComputedElementRef,
{ window = defaultWindow, scrollTarget }: UseElementVisibilityOptions = {},
) {
const elementIsVisible = ref(false)
const testBounding = () => {
if (!window)
return
const document = window.document
const el = unrefElement(element)
if (!el) {
elementIsVisible.value = false
}
else {
const rect = el.getBoundingClientRect()
elementIsVisible.value = (
/**
* 这个条件检查元素的顶部是否在视窗的高度内。
* window.innerHeight提供了视窗的当前高度(包括滚动条),但如果它不可用(在某些旧版浏览器中),
* 则使用document.documentElement.clientHeight作为回退,这表示整个文档的高度(在标准模式下等同于视窗高度)。
*/
rect.top <= (window.innerHeight || document.documentElement.clientHeight)
&& rect.left <= (window.innerWidth || document.documentElement.clientWidth)
/**
* bottom 是元素底部到视口顶部的距离
*/
&& rect.bottom >= 0
&& rect.right >= 0
)
}
}
/**
* 1 监听元素的变化,会触发testBounding
* post:时机是页面更新之后,因为这个是dom引用,需要dom渲染之后才能有值
*/
watch(
() => unrefElement(element),
() => testBounding(),
{ immediate: true, flush: 'post' },
)
/**
* 2 监听容器的滚动事件,触发testBounding,
* capture: false,在冒泡阶段执行,也是延迟触发时机的
* passive: true,显式告诉浏览器,回调中不会阻止默认事件。这有利于减少浏览器等待时间,提高性能。
*/
if (window) {
//
useEventListener(scrollTarget || window, 'scroll', testBounding, {
capture: false, passive: true,
})
}
return elementIsVisible
}
- 再看一下指令的代码
// 符合自定义指令定义的规范 https://cn.vuejs.org/guide/reusability/custom-directives.html
export const vElementVisibility: ObjectDirective<
HTMLElement,
BindingValueFunction | BindingValueArray
> = {
// 只注册了mounted这个钩子,使用变量是为了适配vue2
[directiveHooks.mounted](el, binding) {
if (typeof binding.value === 'function') {
const handler = binding.value
const isVisible = useElementVisibility(el)
watch(isVisible, v => handler(v), { immediate: true })
}
else {
/**
* isVisible变化时,执行回调。在官方示例中,就是执行 isVisible.value = state
*/
const [handler, options] = binding.value
const isVisible = useElementVisibility(el, options)
watch(isVisible, v => handler(v), { immediate: true })
}
},
}
⚠️:这个函数没有卸载清理的方法,会不会有问题?