一文带你认识页面中的各种距离

400 阅读9分钟

前言

在前端开发过程当中,‌理解和使用页面上的各种距离是非常重要的,‌因为它们直接关系到元素的位置和布局。‌除了常见的外边距(margin)和内边距(padding)等,还会涉及到一些与元素位置和滚动相关的距离属性。这些属性可以帮助我们更好的控制和获取元素的位置、尺寸以及滚动状态等,从而实现更加精细的布局和交互效果。接下来我们先来看看都有哪些常见的应用场景:

  • 固定导航栏

当用户向下滚动页面到达设置的阈值时,导航栏就会固定在顶部,例如B站的导航栏:

fixed-nav.gif

这里就需要知道文档向下滚动的距离, 通过与阈值进行比较来进行相应的操作(添加样式/移除样式)。

  • 返回顶部按钮

image.png

掘金的“返回顶部按钮”,就是通过监听滚动事件,判断文档在垂直方向已滚动的像素值是否大于800来决定是否展示“返回顶部按钮”。

  • 无限滚动:当滚动到页面底部时,加载更多内容.
  • 拖拽元素:允许用户拖拽元素并放置在新的位置.
  • 模态窗口居中:确保模态窗口在浏览器窗口的中间.
  • 计算元素的可见性:判断一个元素是否在视口中,以便延迟加载图片等.
  • Popover组件的浮层位置.
  • ...

可以看到,网页中的距离在我们日常开发中是非常常用的,因此我们有必要去了解一下常见的各个距离。

视口Viewport

在了解具体的距离属性之前,我们需要先知道什么是视口,根据MDN上的解释可以看到:

image.png

可以看到,视口在浏览器中的具体含义就是:页面上可见内容的部分

image.png

一般我们所说的视口可以划分为三种:布局视口、视觉视口和理想视口。 关于更具体的一些说明,大家可以看看这片文章视口.

我们不需要理解的太细,只需要知道下文中提到的视口,表示页面上可见内容的那部分区域。

元素位置相关属性

1、offset

  • offsetTop: 元素的上外边距与其offsetParent节点的上内边距之间的距离
  • offsetLeft: 元素的左外边距与其offsetParent节点的左内边距之间的距离
  • offsetWidth: 元素的布局宽度,包括内边距、滚动条(如果存在)和边框(不包括外边距)
  • offsetHeight: 元素的布局高度,包括内边距、滚动条(如果存在)和边框(不包括外边距)

上面提到的offsetParent又是什么东西呢?

  • offsetParent: 距离元素最近的一个具有定位的祖先元素(relativeabsolutefixed)

举个例子:

<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;
}

我们现在打印一下,看看sonoffsetParent和它的offsetTop

const son = document.querySelector('.son') as HTMLDivElement;
console.log(son?.offsetParent, son?.offsetTop);

image.png

可以看到,由于son的父容器、父容器的父容器没有设置定位,所以它的offsetParent会找到body元素。

它的offsetTop就会取到body的内边距为止,因为该例子中没有给body设置内边距,所以只会取到grandfather的外边距为止。

image.png

现在我们给grandfather加上定位,再试试,

.grandfather {
    /*...*/
    position: relative;
}

image.png

可以看到offsetParent现在变成了grandfather这个容器,且它的offsetTop取值只会取道grandfather容器的内边距。

image.png

同理offsetLeft的计算也是这样计算的,从当前元素的外边距开始到offsetParent的左内边距为止。

而元素的offsetHeightoffsetWidth正如说明的那样,取的是元素的布局宽度/高度(不包括外边距)

const son = document.querySelector('.son') as HTMLDivElement;
console.log(son?.offsetHeight, son.offsetWidth);

image.png

image.png

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);

image.png

3、scroll

  • scrollTop: 元素的内容垂直滚动的像素数(内容顶部到它的视口可见内容(的顶部)的距离)
  • scrollLeft: 元素的内容水平滚动的像素数(内容左侧到它的视口可见内容(的左侧)的距离)
  • scrollWidth: 元素内容的整体宽度,包括看不见的部分
  • scrollHeight: 元素内容的整体高度,包括看不见的部分

image.png

以上图片来自MDN

应用场景

通过监听滚动事件并比较 scrollTopclientHeightscrollHeight,可以判断用户是否已经阅读了整个文本内容。这种方法可以用于各种需要确认用户已经阅读内容的场景,如同意条款、显示隐藏内容等。

if (content.scrollTop + content.clientHeight >= content.scrollHeight) {
    // ...
}

窗口滚动相关属性

  • window.scrollX: 文档/页面水平方向滚动的像素值
  • window.scrollY: 文档/页面垂直方向已滚动的像素值
  • window.pageXOffset:页面水平滚动的像素数(与 window.scrollX 相同),该属性属性比较老的属性,现在已经不推荐使用了
  • window.pageYOffset: 页面垂直滚动的像素数(与 window.scrollY 相同),该属性属性比较老的属性,现在已经不推荐使用了

image.png

可以从定义中看出,window.scrollY表示文档在垂直方向上滚动的像素,而前面我们刚学过的element.scrollTop表示元素的内容垂直滚动的像素数, 如果我们取根元素的scrollTop,是不是和window.scrollY大小一致,我们来试试:

image.png

可以看到,确实是这样的。

应用场景:判断文档是否滚动到底部了

通过document.documentElement.scrollTop(window.scrollY) + window.innerHeihgtdocument.documentElement.scrollHeight对比

if (document.documentElement.scrollHeight === document.documentElement.scrollTop + window.innerHeight) { 
    console.log('滚动到底了');
}
  • window.innerHeight: 浏览器窗口的视口(viewport)高度(以像素为单位);如果有水平滚动条,也包括滚动条高度。

image.png

鼠标位置相关属性

  • event.offsetXevent.offsetY:鼠标距离触发事件元素左侧/顶部的距离(相对于事件源元素的坐标)
  • event.clientXevent.clinetY:鼠标距离可视区域左侧/顶部的距离(相对于视口的坐标,不考虑滚动)
  • event.pageXevent.pageY:鼠标距离文档左侧/顶部的距离(相对于整个文档的坐标)
  • event.screenXevent.screenY:鼠标距离屏幕左侧/顶部的距离(相对于整个屏幕的垂直坐标)

image.png

Element.getBoundingClientRect()

返回一个DOMRect 对象,其提供了元素的大小及其相对于视口的位置。

image.png

以上图片来自MDN

结合鼠标的位置信息来看可以看出:

image.png

  • event.offsetY === event.clientY - rect.top/rect.y; // true

有的同学会问,这有什么用呢?别急,下面这个例子带你了解一下:

  1. 首先我们通过npx create-vite创建一个vite项目,
npx create-vite react-distance

image.png

  1. 进入react-distance, 安装依赖npm i/pnpm i
  2. 修改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属性.

image.png

这是怎么一回事呢?

原来,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: 距离元素最近的一个具有定位的祖先元素(relativeabsolutefixed)
    • 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.offsetXevent.offsetY:鼠标距离触发事件元素左侧/顶部的距离(相对于事件源元素的坐标)
    • event.clientXevent.clinetY:鼠标距离可视区域左侧/顶部的距离(相对于视口的坐标,不考虑滚动)
    • event.pageXevent.pageY:鼠标距离文档左侧/顶部的距离(相对于整个文档的坐标)
    • event.screenXevent.screenY:鼠标距离屏幕左侧/顶部的距离(相对于整个屏幕的垂直坐标)
  • Element.getBoundingClientRect(): 可以拿到widthheighttopleft等属性,其中topleft是元素距离可视区域顶部/左侧的距离, 结合鼠标的位置属性我们可以计算出其它距离,例如offsetY.

知道了这些距离,就足够处理日常的开发工作了。