总流程图
主要步骤是获取需要滚动到位置元素的顶部距离,然后使用 ELement 的 scrollTo 方法滚动到指定位置。
详细流程图
(1)首先对滚动容器进行获取,没有传入滚动元素 scrollerid 或获取不到 scrollerid 的 ELement 节点则使用 body 作为滚动的容器进行滚动。
(2)获取滚动容器的 scrollTop 和元素的 offsetTop 进行大小对比,判断是向上还是向下滚动。
(3)window.requestAnimationFrame 设置的滚动动画每一帧都会传入当前的时间戳,根据(和第一帧时间戳的差值 / 动画的时间)* |scrollTop - offsetTop|
来获取每一帧的滚动位置。
(4)判断和第一帧时间戳的差值 - 动画的时间 >= 0
来判断动画时间是不是已经到了,否则动画会一直执行。
(5)当判断到动画时间结束,需要补上最后一帧的动画
,原因是当使用和第一帧时间戳的差值 - 动画的时间 >= 0
判断最后一帧的动画,可能出现最后一帧和第一帧差值 !== 动画的时间 && 下一帧和第一帧的差值 〉 动画的时间
,就会导致其实有几毫秒的距离是没有滚动的。这个时候需要补上真正的最后一帧的滚动动画
。
具体代码
export const scrollTo = (params = {}) => {
const baseConfig = {
duration: 500, // 滚动的动画的时间
scroller: '', // 滚动容器的 id ,默认为 body
id: '', // 滚动到指定位置的元素 id
};
const config = {
...baseConfig,
...params,
};
const { id, duration, scroller } = config;
const ele = document.getElementById(id);
if (ele) {
const { scrollTop, scrollerOffsetTop } = getScrollTop(scroller);
// 需要减去滚动容器到顶部的距离,默认滚动容器是 body ,scrollerOffsetTop 的值为 0
const offsetTop = ele.offsetTop - scrollerOffsetTop;
let start;
let timeOff = false;
const scrollFrame = (timestramp) => {
if (start === undefined) {
start = timestramp;
}
const elapsed = timestramp - start;
const offset = getOffset({
scrollTop,
offsetTop,
elapsed,
duration,
});
if (duration - elapsed >= 0) {
window.requestAnimationFrame(scrollFrame);
scolling(scroller, offset);
} else {
// 最后一帧因为时间判断可能不会执行,补上最后一帧
if (!timeOff) {
scolling(scroller, offsetTop);
timeOff = true;
}
}
};
window.requestAnimationFrame(scrollFrame);
}
};
const getScrollTop = (scrollerTag) => {
let scrollTop;
let scrollerOffsetTop = 0; // 滚动容器距离 body 顶部的距离
const ele = document.getElementById(scrollerTag);
if (ele) {
scrollTop = ele.scrollTop;
scrollerOffsetTop = ele.offsetTop;
} else {
scrollTop
= document.documentElement.scrollTop
|| window.pageYOffset
|| document.body.scrollTop;
}
return { scrollTop, scrollerOffsetTop };
};
const getOffset = (data) => {
const { scrollTop, offsetTop, elapsed, duration } = data;
let offset;
// 判断是向下还是向上滚动
if (scrollTop > offsetTop) {
offset = scrollTop - (elapsed / duration) * (scrollTop - offsetTop);
} else {
offset = scrollTop + (elapsed / duration) * (offsetTop - scrollTop);
}
return offset;
};
const scolling = (scrollerTag, offset) => {
const scroller = getScroller(scrollerTag);
scroller.scrollTo(0, offset);
};
const getScroller = scrollerTag => document.getElementById(scrollerTag) || window;
示例 vue 文件
<template>
<div>
<div class="select-list">
<div
class="select-item"
v-for="item in selectList"
:key="item"
@click="handleScroll(item)"
>
{{ item }}
</div>
</div>
<div class="scroller" id="scroller">
<div
class="scroller-child"
v-for="(item, index) in selectList"
:key="item"
:id="baseId + item"
:style="{ background: colorList[index] }"
>
{{ item }}
</div>
</div>
</div>
</template>
<script>
import { scrollTo } from './secoll';
export default {
data() {
return {
selectList: ['1', '2', '3', '4'],
colorList: ['black', 'white', 'blue', 'green'],
baseId: 'scroller-child_',
};
},
methods: {
handleScroll(item) {
scrollTo({
id: this.baseId + item,
// 注释掉不是 body 为滚动容器
// scroller: 'scroller',
});
},
},
};
</script>
<style lang="less" scoped>
.select-list {
display: flex;
justify-content: space-around;
flex-direction: row;
font-size: 24px;
position: fixed;
top: 0;
width: 100%;
}
.scroller {
margin-top: 40px;
// 注释掉不是 body 为滚动容器
// max-height: 600px;
// overflow-y: scroll;
}
.scroller-child {
margin: 0 20px 20px 20px;
border: 1px solid olivedrab;
height: 1550px;
}
</style>