在前端开发中,计算各种距离和尺寸是常见需求。以下是一些典型场景和相关属性的详细介绍。
1 常见场景
- OnBoarding 组件:需要获取每一步高亮元素的位置和尺寸。
- Popover 组件:需要获取元素位置以确定浮层位置。
- 滚动加载:需要获取滚动距离和页面高度以判断是否触底。
2 浏览器中的距离和宽高属性
2.1 可视区域与滚动条
页面通常超过一屏,右侧会出现滚动条,表示当前可视区域的位置。可视区域也称为视口(viewport)。
2.2 鼠标事件位置属性
当点击可视区域内的元素时,可以通过事件对象获取以下属性:
pageY
:点击位置到文档顶部的距离。clientY
:点击位置到可视区域顶部的距离。offsetY
:点击位置到触发事件元素顶部的距离。screenY
:点击位置到屏幕顶部的距离。
2.3 示例代码
// 导入必要的 React 钩子
import { MouseEventHandler, useEffect, useRef } from 'react';
function App() {
// 创建一个 ref 用于引用 DOM 元素
const boxRef = useRef<HTMLDivElement>(null);
// 定义点击事件处理函数
const handleClick: MouseEventHandler<HTMLDivElement> = event => {
if (boxRef.current) {
const boxRect = boxRef.current.getBoundingClientRect();
const offsetY = event.clientY - boxRect.top;
console.log('鼠标事件坐标信息:');
console.log('pageY (相对于整个文档):', event.pageY);
console.log('clientY (相对于浏览器视口):', event.clientY);
console.log('offsetY (相对于目标元素):', offsetY);
console.log('screenY (相对于屏幕):', event.screenY);
}
};
// 使用 useEffect 钩子添加原生事件监听器
useEffect(() => {
const boxElement = boxRef.current;
if (boxElement) {
const nativeClickHandler = (e: MouseEvent) => {
console.log('原生事件监听器获取的坐标信息:');
console.log('pageY:', e.pageY);
console.log('clientY:', e.clientY);
console.log('offsetY:', e.offsetY);
console.log('screenY:', e.screenY);
};
boxElement.addEventListener('click', nativeClickHandler);
// 清理函数
return () => {
boxElement.removeEventListener('click', nativeClickHandler);
};
}
}, []); // 空依赖数组表示这个效果只在组件挂载时运行一次
return (
<div>
<div
ref={boxRef}
style={{
marginTop: '800px', // 设置上边距,使元素远离页面顶部
width: '100px',
height: '100px',
background: 'blue',
}}
onClick={handleClick} // 绑定点击事件处理函数
></div>
</div>
);
}
export default App;
2.4 React 合成事件的坑点
React 的合成事件缺少一些原生事件属性,如 offsetY
。
可以使用 react-use
提供的 useMouse
hook 来解决:
const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
const top = document.getElementById('box')!.getBoundingClientRect().top;
console.log('box offsetY', e.pageY - top - window.scrollY);
};
2.5 元素的滚动距离
window.scrollY
:窗口滚动的距离。element.scrollTop
:元素内容的滚动距离。
2.6 示例代码
import { MouseEventHandler, useRef, useEffect } from 'react';
function App() {
const containerRef = useRef<HTMLDivElement>(null);
const boxRef = useRef<HTMLDivElement>(null);
useEffect(() => {
const handleScroll = () => {
console.log('窗口滚动距离:', window.scrollY);
};
window.addEventListener('scroll', handleScroll);
return () => {
window.removeEventListener('scroll', handleScroll);
};
}, []);
const clickHandler: MouseEventHandler<HTMLDivElement> = () => {
console.log('元素内容滚动距离:', boxRef.current?.scrollTop);
};
return (
<div ref={containerRef} style={{ height: '2000px' }}>
<h2 style={{ position: 'fixed', top: 0, left: 0 }}>滚动页面和内部元素来查看效果</h2>
<div
ref={boxRef}
style={{
marginTop: '800px',
width: '300px',
height: '200px',
background: 'pink',
overflow: 'auto',
padding: '10px',
}}
onClick={clickHandler}
>
<p>点击此元素查看内部滚动距离</p>
{Array(20)
.fill(null)
.map((_, index) => (
<p key={index}>这是第 {index + 1} 行内容</p>
))}
</div>
</div>
);
}
export default App;
2.7 元素的其他属性
offsetTop
:元素距离最近的有position
属性的父元素的内容顶部的距离。clientTop
:元素上边框的高度。
2.8 示例代码
import { useEffect, useRef, useState } from 'react';
// App 组件:展示一个带有边框的盒子,并显示其 offsetTop 和 clientTop 值
function App() {
// 创建一个引用,用于访问 DOM 元素
const ref = useRef<HTMLDivElement>(null);
// 状态hooks,用于存储 offsetTop 和 clientTop 值
const [offsetTop, setOffsetTop] = useState<number | undefined>(undefined);
const [clientTop, setClientTop] = useState<number | undefined>(undefined);
// 副作用hook,在组件挂载后获取并设置 offsetTop 和 clientTop 值
useEffect(() => {
if (ref.current) {
setOffsetTop(ref.current.offsetTop);
setClientTop(ref.current.clientTop);
}
}, []);
return (
// 外层容器,设置相对定位和边框
<div
style={{
position: 'relative',
margin: '50px',
padding: '100px',
border: '1px solid blue',
}}
>
<p>
offsetTop: {offsetTop}px {/* offsetTop 表示元素相对于其定位父元素的顶部偏移量 */}
</p>
<p>
clientTop: {clientTop}px {/* clientTop 表示元素上边框的宽度 */}
</p>
{/* 内部盒子,应用ref并设置样式 */}
<div
id="box"
ref={ref}
style={{
position: 'relative',
top: '30px',
left: '20px',
border: '10px solid #000',
width: '100px',
height: '100px',
background: 'pink',
}}
></div>
</div>
);
}
export default App;
2.9 计算元素到根元素的总距离
/**
* 计算元素相对于文档顶部的总偏移量
* @param element 要计算偏移量的HTML元素
* @returns 元素相对于文档顶部的总偏移量(以像素为单位)
*/
function getTotalOffsetTop(element: HTMLElement): number {
let totalOffsetTop = 0;
while (element) {
// 累加元素的边框宽度(除了第一个元素)
if (totalOffsetTop > 0) {
totalOffsetTop += element.clientTop;
}
// 累加元素的offsetTop
totalOffsetTop += element.offsetTop;
// 移动到父级定位元素
element = element.offsetParent as HTMLElement;
}
return totalOffsetTop;
}
测试下:
import { useRef, useEffect, useState } from 'react';
import { getTotalOffsetTop } from './assets/utils';
function App() {
const targetRef = useRef<HTMLDivElement>(null);
const [offsetTop, setOffsetTop] = useState<number | null>(null);
useEffect(() => {
if (targetRef.current) {
const offset = getTotalOffsetTop(targetRef.current);
setOffsetTop(offset);
}
}, []);
return (
<div style={{ padding: '50px' }}>
<h1>getTotalOffsetTop 示例</h1>
<div style={{ marginTop: '100px', border: '1px solid black', padding: '20px' }}>
<div ref={targetRef} style={{ backgroundColor: 'lightblue', padding: '10px' }}>
目标元素
</div>
</div>
{offsetTop !== null && <p>目标元素距离文档顶部的偏移量:{offsetTop}px</p>}
</div>
);
}
export default App;
3 总结
掌握以下属性,可以处理各种需要计算位置和尺寸的需求:
e.pageY
:鼠标距离文档顶部的距离。e.clientY
:鼠标距离可视区域顶部的距离。e.offsetY
:鼠标距离触发事件元素顶部的距离。e.screenY
:鼠标距离屏幕顶部的距离。window.scrollY
:页面滚动的距离。element.scrollTop
:元素内容的滚动距离。element.clientTop
:元素上边框的高度。element.offsetTop
:元素距离最近有position
属性的父元素的内容顶部的距离。clientHeight
:内容高度,不包括边框。offsetHeight
:包含边框的高度。scrollHeight
:滚动区域的高度,不包括边框。window.innerHeight
:窗口的高度。element.getBoundingClientRect()
:获取元素的宽高和位置。
通过这些属性,可以精确计算和处理各种前端布局和交互需求。