图解浏览器的各种距离

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

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

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

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

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

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

今天我们来整体过一遍。

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

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

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

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

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

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

我们试一下:

npx create-vite

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

然后改下 App.tsx

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 么:

试一下:

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 获取。

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 这几个高度要区分下:

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 对比。

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:鼠标距离触发事件元素顶部的距离
  • e.screenY:鼠标距离屏幕顶部的距离
  • 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 通关秘籍》

相关推荐

  • 小学生都开始学 Python 了!
  • OpenAI CEO:AI 成本可降至“接近零”;李飞飞团队发布《2024年人工智能指数报告》 | 极客头条
  • 又一个 GPT-4 级 LLM!前谷歌大脑成员初创公司官宣 Reka Core,多模态交互超越 Claude3 Opus
  • 有奖调查 | 2024 中国开发者大调查正式启动!
  • 想当“iPhone杀手”的首款 AI 硬件 Ai Pin 意外“翻车”:看时间似乎成为它唯一正确可做的事情!
  • 买了小米 SU7 ,被公司强制开除!
  • 让玩家全程掌控游戏:自然语言指令驱动的游戏引擎到来了
  • 无向图最小割问题取得新突破,谷歌研究获SODA 2024最佳论文奖
  • 再见!波士顿动力人形机器人Atlas​
  • Meta无限长文本大模型来了:参数仅7B,已开源
  • 首个国产音乐SOTA模型来了!专为中文优化,免费用,不限曲风
  • Python 离群值检测算法--ECOD
  • 设计数据库就像画画一样简单!揭秘drawDB在线工具
  • 解释因果推断中的“do”操作符
  • 什么开源PUA大师语录:“你想参与开源项目?不,你并不想”
  • 开源项目警惕被“社工”渗透,否则后门不保!
  • 深入理解Transformer技术原理
  • 他潜伏三年想插它后门,最终还是输给了另一个他
  • 用Python写个自动批改作业系统!
  • 50W,确实可以封神了!