Skip to content

网页中的距离


当页面超过视口高度的时候,右侧会出现滚动条,代表当前可视区域的位置。

bilibili

窗口部分也就是视口,viewport。

page

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

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

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

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

代码测验

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);
    // React事件是合成事件,会少一些原生事件的属性,所以这里拿不到e.offsetY
    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 事件中如何获取 offsetY 呢?

看一幅图就知道啦!

offsetY

我们可以使用 pageY - window.scrollY - getBoundingClientRect().top 计算出 offsetY

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

getBoundingClientRect

提示

element.getBoundingClientRect:拿到 width、height、top、left 属性,其中 top、left 是元素距离可视区域的距离,

width、height 绝大多数情况下等同 offsetHeight、offsetWidth,但旋转之后就不一样了,拿到的是包围盒的宽高

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

tsx
const clickHandler: MouseEventHandler<HTMLDivElement> = (e) => {
  console.log("box pageY", e.pageY);
  console.log("box clientY", e.clientY);
  // 访问原生事件对象
  console.log("box offsetY", e.nativeEvent.offsetY);
  console.log("box screenY", e.screenY);
};

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

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

此外,元素还有 offsetTopclientTop 属性:

tsx
import { useEffect, useRef } from "react";

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

  useEffect(() => {
    console.log("offsetTop", ref.current?.offsetTop);
    console.log("clientTop", ref.current?.clientTop);
  }, []);

  return (
    <div>
      <div
        style={{
          position: "relative",
          margin: "100px",
          padding: "200px",
          border: "1px solid blue",
        }}
      >
        <div
          id="box"
          ref={ref}
          style={{
            border: "20px solid #000",
            width: "100px",
            height: "100px",
            background: "pink",
          }}
        ></div>
      </div>
    </div>
  );
}

export default App;
clientTop

可以看到,clientTop 也就是上边框的高度 20px。

offsetTop 是距离最近的有 position 属性(relative 或 absolute 或 fixed)的元素的距离。

所以是 200px。

注释掉就是 301px 了,这时候就是相对于文档顶部,所以是 200px padding+ 1px border + 100px margin。

301

offsetTop 相对于哪个元素,那个元素就是 offsetParent

还可以递归累加到 offsetParentoffsetTop ,直到 offsetParentnull,也就是到了根元素,这时候算出来的就是元素到根元素的 offsetTop:

tsx
import { useEffect, useRef } from "react";

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

  function getTotalOffsetTop(element: HTMLElement) {
    let totalOffsetTop = 0;
    while (element) {
      totalOffsetTop += element.offsetTop;
      element = element.offsetParent as HTMLElement;
    }
    return totalOffsetTop;
  }

  useEffect(() => {
    console.log("offsetTop", ref.current?.offsetTop);
    console.log("clientTop", ref.current?.clientTop);

    console.log("totol offsetTop", getTotalOffsetTop(ref.current!));
  }, []);

  return (
    <div>
      <div
        style={{
          position: "relative",
          margin: "100px",
          padding: "200px",
          border: "1px solid blue",
        }}
      >
        <div
          id="box"
          ref={ref}
          style={{
            border: "20px solid #000",
            width: "100px",
            height: "100px",
            background: "pink",
          }}
        ></div>
      </div>
    </div>
  );
}

export default App;

但是你会发现它少计算了 border 的宽度:

因为 offsetTop 元素顶部到 offsetParent 内容部分的距离,不包括 border。

这时候加上 clientTop 就可以了,它就是上边框的高度。

tsx
function getTotalOffsetTop(element: HTMLElement) {
  let totalOffsetTop = 0;
  while (element) {
    // 最初的元素不用加,因为是算他到别的元素的距离
    if (totalOffsetTop > 0) {
      totalOffsetTop += element.clientTop;
    }
    totalOffsetTop += element.offsetTop;
    element = element.offsetParent as HTMLElement;
  }
  return totalOffsetTop;
}

clientHeight:内容高度,不包括边框

clientHeight

offsetHeight:包含边框的高度

offsetHeight

scrollHeight:滚动区域的高度,不包括边框

scrollHeight

window.innerHeight:窗口的高度

innerHeight

Crafted with care.