Appearance
vue 实现快速实现 Sticky 组件
前言
我们在实际开发过程中,多多少少接触过吸顶的交互场景。
相信大家第一感觉就是很简单啊,用 css
的 sticky
完全满足,我也是这么想的。但是实际在使用的时候会发现 sticky
在快速滑动的时候会有细微的 抖动
,在 移动端尤其明显。
于是想起了 UI库(antd、elementui) 有对吸顶相关的组件(Affix),于是粗略看了相关代码,发现他们不约而同的使用 js
实现了 sticky
的效果,至于为什么都不用 CSS 原生 Sticky 实现,相信现在大家心里都明白了。
所以今天我们说一下如何通过 js 实现 sticky
的效果。
实现
实现之前,我们先分析一下吸顶的具体交互过程:
- 当
目标Dom底部
消失在可视区域的时候,将目标Dom
的position属性
设为fixed
,top属性
设为0
(这里top根据实际情况),z-index属性
设为1
(这里根据实际情况) - 当
目标Dom底部
出现在可视区域的时候,将目标Dom
的position
、top
、z-index
还原。(position: static;top: auto;z-index: auto)
分析完成后,我们发现第一步需要监听 目标Dom底部
是否出现在可视区域,常用的两种方式:
- 使用
IntersectionObserver
监听目标Dom底部
是否出现在可视区域 - 监听
scroll
事件,判断目标Dom底部
是否出现在可视区域
IntersectionObserver
const observer = new IntersectionObserver((entries)=> {
if (entries[0].isIntersecting) { // 出现在可视区域
Object.assign(target.style, {
position: 'static',
zIndex: 'auto',
top: 'auto'
})
} else { // 消失在可视区域
Object.assign(target.style, {
position: 'fixed',
zIndex: 1,
top: 0
})
}
});
observer.observe(target);
我们拆步骤分析一下:
- 1、当
目标Dom
target 消失在可视区域时,我们将position属性
设为fixed
且top属性
设为0
,此时消失的目标Dom
脱离文档流吸顶; - 2、与此同时
消失的目标Dom
脱离文档流后,又出现在了可视区域
,此时又会将position属性
设为static
,消失的目标Dom
又回归到文档流,消失在可视区域 - 3、往复执行……,形成死循环
根据上面分析 IntersectionObserver
显示不适合做吸顶的场景。
scroll
首先我们需要获取到 目标Dom底部
距离页面顶部的距离,当滚动条的滚动距离超过 目标Dom底部
,我们视为 消失在可视区域
,让 目标Dom
脱离文档流完成吸顶;否则,视为 出现在可视区域
,让 目标Dom
回归文档流取消吸顶,具体实现如下:
const debounce = (func: (e: Event) => void, wait: number, immediate?: boolean) => {
let timeout: NodeJS.Timeout | null;
return (...args: any[] | any) => {
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) {
func.apply(this, args);
}
}
else {
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
}
};
};
const target = document.querySelector('xxx')
const rect = target.getBoundingClientRect();
window.addEventListener('scroll', debounce(() => {
let style = { position: 'static', top: 'auto', zIndex: 'auto' };
if (document.documentElement.scrollTop > rect.bottom) {
style = { position: 'fixed', top: `${props.top}`, zIndex: `${props.zIndex}` };
}
Object.assign(target.style, style);
}, 15));
上面使用 debounce
函数进行 防抖
,减少频繁触发的频率。
vue 封装的sticky组件
<template>
<div ref="stickyRef">
<slot />
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
const stickyRef = ref();
defineOptions({
name: 'Sticky'
});
const props = defineProps({
zIndex: {
type: Number,
default: 99
},
top: {
type: Number,
default: 0
}
});
const debounce = (func: (e: Event) => void, wait: number, immediate?: boolean) => {
// eslint-disable-next-line no-undef
let timeout: NodeJS.Timeout | null;
return (...args: any[] | any) => {
if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) {
func.apply(this, args);
}
}
else {
timeout = setTimeout(() => {
func.apply(this, args);
}, wait);
}
};
};
onMounted(() => {
const rect = stickyRef.value.getBoundingClientRect();
window.addEventListener('scroll', debounce(() => {
let style = { position: 'static', top: 'auto', zIndex: 'auto' };
if (document.documentElement.scrollTop > rect.bottom) {
style = { position: 'fixed', top: `${props.top}`, zIndex: `${props.zIndex}` };
}
Object.assign(stickyRef.value.style, style);
}, 15));
});
</script>