前言
应用场景:
- 我们经常见到的在一些软件初始页面的条约申明阅读,用户必须要浏览完这页的申明才能进行下一步,这里面就会检测这元素是否有滑动到底部。
- 根据下拉还是下拉去实现其他的功能(如:下拉时标题需要消失,上拉时显示)等等
根据上面的场景,尝试封装一个scroll组件及hook。
知识回顾
开始前,我们需要回顾一下元素样式的四个属性,可以自行查阅更多相关的属性。
offsetHeight:一个元素能够展示其所有内容所需要的最小高度,是元素整个的content加上padding的高度,不包括border。如果元素内容超过可视区域,可以想象成将整个元素撑开的高度。
scrollHeight:一个元素的content+padding+border+margin+scroll bar的高度。也是在可视范围内这些高度的相加。
offsetTop:当前对象到其上级层顶部的距离。
scrollTop:对象的最顶部到对象在当前窗体显示的范围内的顶边的距离 即是在出现了纵向滚动栏的情况下,滚动栏拉动的距离。
React操作Dom的几种方式:传入字符串,传入一个对象(react推荐的方式),传入一个函数。今天这里使用react封装过的高阶组件forwardRef来操作DOM
分析
组件:
实现
type Props = React.PropsWithChildren<{
onScrolled(direction: 'up' | 'down'): void;
toTop?:any
}>;
const ScrollWrap: ForwardRefRenderFunction<HTMLDivElement, Props> = (props, ref: any = {}) => {
const { children, toTop, onScrolled } = props;
const {
locale: { carddetail },
lang
} = useContext(LocaleContext);
useScroll(ref.current, onScrolled);
useEffect(() => {
const $root = $(ref.current);
$root.animate({ scrollTop: 0 });
}, [toTop]);
return (
<div className={classes.root} div-mark={'scrollmark'} ref={ref}>
{children}
</div>
);
};
export default React.memo(forwardRef(ScrollWrap));
如上,children相当于插槽,然后通过ref拿到实例。通过是否有toTop的的更新来判断是否要让滚动条回到顶部。
主要逻辑:useScroll
type DomPositions = [number, number, number, number]; //offsetTop,offsetHeight,scrollTop,scrollHeight;
const positionChanged = (before: DomPositions, after: DomPositions) => {
//如果offsetHeight + scrollTop 的值不变,说明scrollBottom没变,则认为position没有变
//如果offsetHeight + scrollTop = scrollHeight 说明此时页面已经拉到了底部
//1.判断是否拉到了底部 如果是 则默认没有动 如果不是 则根据scrollTop判断
//return 0 不动,1下拉 -1上拉
if (Math.abs(after[1] + after[2] - after[3]) < 10) {
return 0;
}
return after[2] - before[2] > 0 ? -1 : 1;
};
const useScroll = (dom: any, onScrolled?: (direction: 'up' | 'down') => void) => {
/**
* 记录此刻状态 offsetTop,scrollTop,offsetHeight,scrollHeight;
*/
const [domPosition, setDomPosition] = useState<DomPositions>([0, 0, 0, 0]);
const [scrollDir, setScrollDir] = useState<API.Components.CardDetailLayoutContent.Content.ScrollDirection>('up');
const debounced = useDebounce(scrollDir, 200);
useEffect(() => {
onScrolled && onScrolled(debounced);
}, [debounced, onScrolled]);
useEffect(() => {
const myFunc = (ev: any) => {
const { offsetTop, scrollHeight, scrollTop, offsetHeight } = ev?.target;
const newPosition: DomPositions = [offsetTop, offsetHeight, scrollTop, scrollHeight];
const pchange = positionChanged(domPosition, newPosition);
if (pchange !== 0) {
setScrollDir(pchange === 1 ? 'up' : 'down');
}
setDomPosition(newPosition);
};
const throttleFunc = throttle(myFunc, 500, 1000, false);
dom?.addEventListener && dom.addEventListener('scroll', throttleFunc);
return () => {
dom?.removeEventListener && dom.removeEventListener('scroll', throttleFunc);
};
}, [dom, domPosition]);
};
必不可少的节流:鼠标移入能立刻执行,停止触发的时候还能再执行一次
function throttle(fun, t, mustRun, denyLast) {
let timer = null;
let startTime = 0;
return function(event) {
// eslint-disable-next-line @typescript-eslint/no-this-alias
let that = this;
// eslint-disable-next-line prefer-rest-params
let args = arguments;
clearTimeout(timer);
let later = function() {
timer = null;
if (denyLast) fun.apply(that, args);
console.log('执行的是later.');
};
let currTime = new Date().getTime();
if (currTime - startTime >= mustRun) {
fun.apply(that, args);
startTime = currTime;
} else {
timer = setTimeout(later, t);
}
};
}
export default throttle;