图解浏览器的各种距离

网页开发中,我们经常要计算各种距离。

比如 OnBoarding 组件,我们要拿到每一步的高亮元素的位置、宽高:

比如 Popover 组件,需要拿到每个元素的位置,然后确定浮层位置:

比如滚动到页面底部,触发列表的加载,这需要拿到滚动的距离和页面的高度。

类似这样,需要计算距离、宽高等的场景有很多。

而浏览器里与距离、宽高有关的属性也有不少。

今天我们来整体过一遍。

首先,页面一般都是超过一屏的,右边会出现滚动条,代表当前可视区域的位置:

这里窗口的部分是可视区域,也叫做视口 viewport。

如果我们点击了可视区域内的一个元素,如何拿到位置信息呢?

我们只看 y 轴方向好了,x 轴也是一样的。

事件对象可以拿到 pageY、clientY、offsetY,分别代表到点击的位置到文档顶部,到可视区域顶部,到触发事件的元素顶部的距离。

还有个 screenY,是拿到到屏幕顶部的距离。

我们试一下:

lua 复制代码
npx create-vite

去掉 main.tsx 的里 index.css 和 StrictMode:

然后改下 App.tsx

javascript 复制代码
import { MouseEventHandler, useEffect, useRef } from 'react'

function App() {
  const ref = useRef<HTMLDivElement>(null);

  const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
    console.log('box pageY', e.pageY);
    console.log('box clientY', e.clientY)
    console.log('box offsetY', e.offsetY);
    console.log('box screenY', e.screenY);
  }

  useEffect(() => {
    document.getElementById('box')!.addEventListener('click', (e) => {
      console.log('box2 pageY', e.pageY);
      console.log('box2 clientY', e.clientY)
      console.log('box2 offsetY', e.offsetY);
      console.log('box2 screenY', e.screenY);
    });
  }, []);

  return (
    <div>
      <div id="box" ref={ref} style={{
        marginTop: '800px',
        width: '100px',
        height: '100px',
        background: 'blue'
      }} onClick={clickHandler}></div>
    </div>
  )
}

export default App

为什么要用两种方式添加点击事件呢?

因为这里要介绍一个 react 事件的坑点:

react 事件是合成事件,所以它少了一些原生事件的属性,比如这里的 offsetY,也就是点击的位置距离触发事件的元素顶部的距离。

你写代码的时候 ts 就报错了:

那咋办呢?

react-use 提供的 useMouse 的 hook 就解决了这个问题:

它是用 e.pageY 减去 getBoundingClientRect().top 减去 window.pageYOffset 算出来的。

这里的 getBoundingClientRect 是返回元素距离可以可视区域的距离和宽高的:

而 window.pageYOffset 也叫 window.scrollY,顾名思义就是窗口滚动的距离。

想一下,pageY 去了 window.scrollY,去了 getBoundingClientRect().top,剩下的可不就是 offsetY 么:

试一下:

javascript 复制代码
const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
    const top = document.getElementById('box')!.getBoundingClientRect().top;

    console.log('box pageY', e.pageY);
    console.log('box clientY', e.clientY)
    console.log('box offsetY', e.pageY - top - window.pageYOffset);
    console.log('box screenY', e.screenY);
}

因为 getBoundingClientRect 返回的数值是更精确的小数,所以算出来的也是小数。

还有,这里的 window.pageYOffset 过时了,简易换成 window.scrollY,是一样的:

当然,你也可以访问原生事件对象,拿到 offsetY 属性:

此外,窗口的滚动距离用 window.scrollY 获取,那元素也有滚动条呢?

元素内容的滚动距离用 element.scrollTop 获取。

javascript 复制代码
import { MouseEventHandler, useEffect, useRef } from 'react'

function App() {
  const ref = useRef<HTMLDivElement>(null);

  const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
    console.log(ref.current?.scrollTop);
  }

  return (
    <div>
      <div id="box" ref={ref} style={{
        marginTop: '800px',
        width: '100px',
        height: '100px',
        background: 'ping',
        overflow: 'auto'
      }} onClick={clickHandler}>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
      </div>
    </div>
  )
}

export default App

给 box 加一些内容,设置 overflow:auto。

试一下:

这样,距离我们就知道怎么算了,就是用 pageY、clientY、screenY、offsetY 综合 getBoundingClientRect 和 window.scrollY 还有 element.scrollTop 来算。

这里 clientY 和 getBoundingClientRect().top 也要区分下:

一个是元素距离可视区域顶部的距离,一个是鼠标事件触发位置到可视区域顶部的距离。

比如页面是否滚动到底部,就可以通过 document.documentElement.scrollTop + window.innerHeihgt 和 document.documentElement.scrollHeight 对比。

这里有涉及到了几个新的属性。

根元素 documentElement 的 scrollTop 就是 window.scrollY:

然后 window.innerHeight、window.innerWidth 是窗口的宽高,也就是可视区域的宽高。

至于 scrollHeight,这是元素的包含滚动区域的高度。

类似的有 clientHeight、offsetHeight、getBoundingClient().height 这几个高度要区分下:

javascript 复制代码
import { MouseEventHandler, useEffect, useRef } from 'react'

function App() {
  const ref = useRef<HTMLDivElement>(null);

  const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
    console.log('clentHeight', ref.current?.clientHeight);
    console.log('scrollHeight', ref.current?.scrollHeight);
    console.log('offsetHeight', ref.current?.offsetHeight);
    console.log('clent rect height', ref.current?.getBoundingClientRect().height);
  }

  return (
    <div>
      <div id="box" ref={ref} style={{
        border: '10px solid #000',
        marginTop: '300px',
        width: '100px',
        height: '100px',
        background: 'pink',
        overflow: 'auto'
      }} onClick={clickHandler}>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
      </div>
    </div>
  )
}

export default App

试一下:

clientHeight 是内容区域的高度,不包括 border。

offsetHeight 包括 border。

scrollHeight 是滚动区域的总高度,不包括 border。

那看起来 getBoundingClientRect().height 和 offsetHeight 一模一样?

绝大多数情况下是的。

但你旋转一下:

就不一样了:

getBoundingClientRect 拿到的包围盒的高度,而 offsetHeight 是元素本来的高度。

所以,对于滚动到页面底部的判断,就可以用 window.scrollY + window.innerHeight 和 document.documentElement.scrollHeight 对比。

javascript 复制代码
import { useEffect, useRef } from 'react'

function App() {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    window.addEventListener('scroll', () => {
      console.log(window.scrollY + window.innerHeight, document.documentElement.scrollHeight);
    })
  }, []);

  return (
    <div>
      <div id="box" ref={ref} style={{
        border: '10px solid #000',
        marginTop: '800px',
        width: '100px',
        height: '100px',
        background: 'pink',
        overflow: 'auto',
        transform: 'rotate(45deg)'
      }}>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
        <p>xxxxx</p>
      </div>
    </div>
  )
}

export default App

这样,浏览器里的各种距离和宽高我们就过了一遍。

总结

浏览器里计算位置、宽高、判断一些交互,都需要用到距离、宽高的属性。

这类属性比较多,我们整体过了一遍:

  • e.pageY:鼠标距离文档顶部的距离
  • e.clientY:鼠标距离可视区域顶部的距离
  • e.offsetY:鼠标距离触发事件元素顶部的距离
  • winwodw.scrollY:页面滚动的距离,也叫 window.pageYOffset,等同于 document.documentElement.scrollTop
  • element.scrollTop:元素滚动的距离
  • clientHeight:内容高度,不包括边框
  • offsetHeight:包含边框的高度
  • scrollHeight:滚动区域的高度,不包括边框
  • window.innerHeight:窗口的高度
  • element.getBoundingClientRect:拿到 width、height、top、left 属性,其中 top、left 是元素距离可视区域的距离,width、height 绝大多数情况下等同 offsetHeight、offsetWidth,但旋转之后就不一样了,拿到的是包围盒的宽高

其中,还要注意 react 的合成事件没有 offsetY 属性,可以自己算,react-use 的 useMouse 的 hook 就是自己算的,也可以用 e.nativeEvent.offsetY 来拿到。

掌握这些宽高、距离属性,就足够处理各种需要计算位置、宽高的需求了。

更多内容可以看我的小册《React 通关秘籍》

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax