前言
在前端开发过程当中,理解和使用页面上的各种距离是非常重要的,因为它们直接关系到元素的位置和布局。除了常见的外边距(margin)和内边距(padding)等,还会涉及到一些与元素位置和滚动相关的距离属性。这些属性可以帮助我们更好的控制和获取元素的位置、尺寸以及滚动状态等,从而实现更加精细的布局和交互效果。接下来我们先来看看都有哪些常见的应用场景:
- 固定导航栏
当用户向下滚动页面到达设置的阈值时,导航栏就会固定在顶部,例如B站的导航栏:
这里就需要知道文档向下滚动的距离, 通过与阈值进行比较来进行相应的操作(添加样式/移除样式)。
- 返回顶部按钮
掘金的“返回顶部按钮”,就是通过监听滚动事件,判断文档在垂直方向已滚动的像素值是否大于800来决定是否展示“返回顶部按钮”。
- 无限滚动:当滚动到页面底部时,加载更多内容.
- 拖拽元素:允许用户拖拽元素并放置在新的位置.
- 模态窗口居中:确保模态窗口在浏览器窗口的中间.
- 计算元素的可见性:判断一个元素是否在视口中,以便延迟加载图片等.
Popover组件的浮层位置.- ...
可以看到,网页中的距离在我们日常开发中是非常常用的,因此我们有必要去了解一下常见的各个距离。
视口Viewport
在了解具体的距离属性之前,我们需要先知道什么是视口,根据MDN上的解释可以看到:
可以看到,视口在浏览器中的具体含义就是:页面上可见内容的部分。
一般我们所说的视口可以划分为三种:布局视口、视觉视口和理想视口。 关于更具体的一些说明,大家可以看看这片文章视口.
我们不需要理解的太细,只需要知道下文中提到的视口,表示页面上可见内容的那部分区域。
元素位置相关属性
1、offset
offsetTop: 元素的上外边距与其offsetParent节点的上内边距之间的距离offsetLeft: 元素的左外边距与其offsetParent节点的左内边距之间的距离offsetWidth: 元素的布局宽度,包括内边距、滚动条(如果存在)和边框(不包括外边距)offsetHeight: 元素的布局高度,包括内边距、滚动条(如果存在)和边框(不包括外边距)
上面提到的offsetParent又是什么东西呢?
offsetParent: 距离元素最近的一个具有定位的祖先元素(relative,absolute,fixed)
举个例子:
<div class="grandfather">
<div class="father">
<div class="son">son</div>
</div>
</div>
.grandfather {
margin: 30px auto;
padding: 30px;
background: orange;
}
.father {
padding: 20px;
background: green;
border: 2px solid black;
}
.son {
width: 100px;
height: 100px;
padding: 10px;
margin: 1px;
border: 1px solid black;
background: aqua;
}
我们现在打印一下,看看son的offsetParent和它的offsetTop
const son = document.querySelector('.son') as HTMLDivElement;
console.log(son?.offsetParent, son?.offsetTop);
可以看到,由于son的父容器、父容器的父容器没有设置定位,所以它的offsetParent会找到body元素。
它的offsetTop就会取到body的内边距为止,因为该例子中没有给body设置内边距,所以只会取到grandfather的外边距为止。
现在我们给grandfather加上定位,再试试,
.grandfather {
/*...*/
position: relative;
}
可以看到offsetParent现在变成了grandfather这个容器,且它的offsetTop取值只会取道grandfather容器的内边距。
同理offsetLeft的计算也是这样计算的,从当前元素的外边距开始到offsetParent的左内边距为止。
而元素的offsetHeight和offsetWidth正如说明的那样,取的是元素的布局宽度/高度(不包括外边距)
const son = document.querySelector('.son') as HTMLDivElement;
console.log(son?.offsetHeight, son.offsetWidth);
2、client
clientTop: 元素顶部边框的宽度(以像素表示)。不包括顶部外边距和内边距.(内联元素为0)clientLeft: 元素左侧边框的宽度(以像素表示)。不包括顶部外边距和内边距.(内联元素为0)clientWidth: 元素的宽度,包括内边距,不包括边框、滚动条和外边距.(内联元素为0)clientHeight: 元素的高度,包括内边距,不包括边框、滚动条和外边距.(内联元素为0)
<style>
.block {
width: 100px;
height: 100px;
padding: 10px;
margin: 20px;
border: 1px solid black;
background: orange;
}
.inline {
padding: 10px;
margin: 10px;
border: 1px solid black;
}
</style>
<div class="block"></div>
<span class="inline">inline box</span>
const blockEl = document.querySelector('.block') as HTMLDivElement;
const inlineEl = document.querySelector('.inline') as HTMLSpanElement;
console.log('块元素的clientTop/Left:', blockEl.clientTop, blockEl.clientLeft);
console.log('块元素的clientWidth/Height:', blockEl.clientWidth, blockEl.clientHeight);
console.log('行内元素的clientTop/Left:', inlineEl.clientTop, inlineEl.clientLeft);
console.log('行内元素的clientWidth/Height:', inlineEl.clientWidth, inlineEl.clientHeight);
3、scroll
scrollTop: 元素的内容垂直滚动的像素数(内容顶部到它的视口可见内容(的顶部)的距离)scrollLeft: 元素的内容水平滚动的像素数(内容左侧到它的视口可见内容(的左侧)的距离)scrollWidth: 元素内容的整体宽度,包括看不见的部分scrollHeight: 元素内容的整体高度,包括看不见的部分
以上图片来自MDN
应用场景
通过监听滚动事件并比较 scrollTop、clientHeight 和 scrollHeight,可以判断用户是否已经阅读了整个文本内容。这种方法可以用于各种需要确认用户已经阅读内容的场景,如同意条款、显示隐藏内容等。
if (content.scrollTop + content.clientHeight >= content.scrollHeight) {
// ...
}
窗口滚动相关属性
window.scrollX: 文档/页面水平方向滚动的像素值window.scrollY: 文档/页面垂直方向已滚动的像素值window.pageXOffset:页面水平滚动的像素数(与window.scrollX相同),该属性属性比较老的属性,现在已经不推荐使用了window.pageYOffset: 页面垂直滚动的像素数(与window.scrollY相同),该属性属性比较老的属性,现在已经不推荐使用了
可以从定义中看出,window.scrollY表示文档在垂直方向上滚动的像素,而前面我们刚学过的element.scrollTop表示元素的内容垂直滚动的像素数, 如果我们取根元素的scrollTop,是不是和window.scrollY大小一致,我们来试试:
可以看到,确实是这样的。
应用场景:判断文档是否滚动到底部了
通过document.documentElement.scrollTop(window.scrollY) + window.innerHeihgt 和 document.documentElement.scrollHeight对比
if (document.documentElement.scrollHeight === document.documentElement.scrollTop + window.innerHeight) {
console.log('滚动到底了');
}
window.innerHeight: 浏览器窗口的视口(viewport)高度(以像素为单位);如果有水平滚动条,也包括滚动条高度。
鼠标位置相关属性
event.offsetX与event.offsetY:鼠标距离触发事件元素左侧/顶部的距离(相对于事件源元素的坐标)event.clientX与event.clinetY:鼠标距离可视区域左侧/顶部的距离(相对于视口的坐标,不考虑滚动)event.pageX与event.pageY:鼠标距离文档左侧/顶部的距离(相对于整个文档的坐标)event.screenX与event.screenY:鼠标距离屏幕左侧/顶部的距离(相对于整个屏幕的垂直坐标)
Element.getBoundingClientRect()
返回一个DOMRect 对象,其提供了元素的大小及其相对于视口的位置。
以上图片来自MDN
结合鼠标的位置信息来看可以看出:
event.offsetY === event.clientY - rect.top/rect.y; // true
有的同学会问,这有什么用呢?别急,下面这个例子带你了解一下:
- 首先我们通过
npx create-vite创建一个vite项目,
npx create-vite react-distance
- 进入
react-distance, 安装依赖npm i/pnpm i - 修改
App.tsx
import { type MouseEventHandler } from 'react';
function App() {
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
console.log(e.offsetX, e.offsetY);
console.log(e.clientX, e.clientY);
console.log(e.pageX, e.pageY);
console.log(e.screenX, e.screenY);
}
return (
<>
<div
style={{ width: 200, height: 200, background: '#58a' }}
onClick={handleClick}
>
click me
</div>
</>
)
}
export default App
这时我们会看到,VSCode提示我们说,MouseEvent上找不到offsetX/offsetY属性.
这是怎么一回事呢?
原来,
React的事件是合成事件,它少了一些原生事件的属性,比如offsetX/offsetY,也就是点击的位置距离触发事件的元素顶部的距离。
关于React合成事件的详解可以看看这篇文章 —— React中的合成事件详解
因此,如果我们要用到offsetX/offsetY属性,需要我们自己计算。当然了,也可以通过nativeEvent属性获取到原生DOM属性。
const handleClick: MouseEventHandler<HTMLDivElement> = (e) => {
console.log(e.clientX, e.clientY);
console.log(e.pageX, e.pageY);
console.log(e.screenX, e.screenY);
console.log(e.nativeEvent.offsetX, e.nativeEvent.offsetY);
}
至此,页面中的各种距离我们就简单的过了一遍。
总结
- 视口:页面上可见内容的部分
- 元素位置相关属性
offsetParent: 距离元素最近的一个具有定位的祖先元素(relative,absolute,fixed)offsetTop: 元素的上外边距与其offsetParent节点的上内边距之间的距离offsetLeft: 元素的左外边距与其offsetParent节点的左内边距之间的距离offsetWidth: 元素的布局宽度,包括内边距、滚动条(如果存在)和边框(不包括外边距)offsetHeight: 元素的布局高度,包括内边距、滚动条(如果存在)和边框(不包括外边距)clientTop: 元素顶部边框的宽度(以像素表示)。不包括顶部外边距和内边距.(内联元素为0)clientLeft: 元素左侧边框的宽度(以像素表示)。不包括顶部外边距和内边距.(内联元素为0)clientWidth: 元素的宽度,包括内边距,不包括边框、滚动条和外边距.(内联元素为0)clientHeight: 元素的高度,包括内边距,不包括边框、滚动条和外边距.(内联元素为0)scrollTop: 元素的内容垂直滚动的像素数(内容顶部到它的视口可见内容(的顶部)的距离)scrollLeft: 元素的内容水平滚动的像素数(内容左侧到它的视口可见内容(的左侧)的距离)scrollWidth: 元素内容的整体宽度,包括看不见的部分scrollHeight: 元素内容的整体高度,包括看不见的部分
- 窗口滚动相关属性
window.scrollX: 文档/页面水平方向滚动的像素值window.scrollY: 文档/页面垂直方向已滚动的像素值(等同于document.documentElement.scrollTop)window.pageXOffset:页面水平滚动的像素数(与window.scrollX相同),该属性属性比较老的属性,现在已经不推荐使用了window.pageYOffset: 页面垂直滚动的像素数(与window.scrollY相同),该属性属性比较老的属性,现在已经不推荐使用了
- 鼠标位置相关属性
event.offsetX与event.offsetY:鼠标距离触发事件元素左侧/顶部的距离(相对于事件源元素的坐标)event.clientX与event.clinetY:鼠标距离可视区域左侧/顶部的距离(相对于视口的坐标,不考虑滚动)event.pageX与event.pageY:鼠标距离文档左侧/顶部的距离(相对于整个文档的坐标)event.screenX与event.screenY:鼠标距离屏幕左侧/顶部的距离(相对于整个屏幕的垂直坐标)
Element.getBoundingClientRect(): 可以拿到width、height、top、left等属性,其中top、left是元素距离可视区域顶部/左侧的距离, 结合鼠标的位置属性我们可以计算出其它距离,例如offsetY.
知道了这些距离,就足够处理日常的开发工作了。