注册

一文彻底搞懂js中的位置计算

引言


文章中涉及到的api列表:

scroll相关Api

client相关Api

offset相关Api

Element.getBoundingClientRectAPi

Window.getComputedStyleApi



我们会结合api定义,知名开源库中的应用场景来逐层分析这些api。足以应对工作中关于元素位置计算的大部分场景。




注意在使用位置计算api时要格外的小心,不合理的使用他们可能会造成布局抖动Layout Thrashing影响页面渲染。



scroll


首先我们先来看看scroll相关的属性和方法。


Element.scroll()


Element.scroll()方法是用于在给定的元素中滚动到某个特定坐标的Element 接口。


element.scroll(x-coord, y-coord)
element.scroll(options)


  • x-coord 是指在元素左上方区域横轴方向上想要显示的像素。
  • y-coord 是指在元素左上方区域纵轴方向上想要显示的像素。

也就是element.scroll(x,y)会将元素滚动条位置滚动到对应x,y的位置。


同时也支持element.scroll(options)方式调用,支持传入额外的配置:


{
left: number,
top: number,
behavior: 'smooth' | 'auto' // 平滑滚动还是默认直接滚动
}

Element.scrollHeight/scrollWidth



  • Element.scrollHeight 这个只读属性是一个元素内容高度的度量,包括由于溢出导致的视图中不可见内容。


scrollHeight 的值等于该元素在不使用滚动条的情况下为了适应视口中所用内容所需的最小高度。 没有垂直滚动条的情况下,scrollHeight值与元素视图填充所有内容所需要的最小值clientHeight相同。包括元素的padding,但不包括元素的border和margin。scrollHeight也包括 ::before 和 ::after这样的伪元素。



换句话说Element.scrollHeight在元素不存在滚动条的情况下是恒等于clientHeight的。


但是如果出现了滚动条的话scrollHeight指的是包含元素不可以见内容的高度,出现滚动条的情况下是scrollHeight恒大于clientHeight



  • Element.scrollWidth 这也是一个元素内容宽度的只读属性,包含由于溢出导致视图中不可以见的内容。


原理上和scrollHeight是同理的,只不过这里是宽度而非高度。



简单来说一个元素如果不存在滚动条,那么他们的scrollclient都是相等的值。如果存在了滚动条,client只会计算出当前元素展示出来的高度/宽度,而scroll不仅仅会计算当前元素展示出的,还会包含当前元素的滚动条隐藏内容的高度/宽度。


clientWidth/height + [滚动条被隐藏内容宽度/高度] = scrollWidth/Height


Element.scrollLeft/scrollTop




  • Element.scrollTop 属性可以获取或设置一个元素的内容垂直滚动的像素数.




  • Element.scrollLeft 属性可以读取或设置元素滚动条到元素左边的距离.





需要额外注意的是: 注意如果这个元素的内容排列方向(direction) 是rtl (right-to-left) ,那么滚动条会位于最右侧(内容开始处),并且scrollLeft值为0。此时,当你从右到左拖动滚动条时,scrollLeft会从0变为负数。



scrollLeft/Top在日常工作中是比较频繁使用关于操作滚动条的相关api,他们是一个可以设置的值。根据不同的值对应可以控制滚动条的位置。


其实这两个属性和上方的Element.scroll()可以达到相同的效果。



在实际工作中如果对于滚动操作有很频繁的需求,个人建议去使用better-scroll,它是一个移动/web端的通用js滚动库,内部是基于元素transform去操作的滚动并不会触发相关重塑/回流。



判断当前元素是否存在滚动条



出现滚动条便意味着元素空间将大于其内容显示区域,根据这个现象便可以得到判断是否出现滚动条的规则。



export const hasScrolled = (element, direction) => {
if (!element || element.nodeType !== 1) return;
if (direction === "vertical") {
return element.scrollHeight > element.clientHeight;
} else if (direction === "horizontal") {
return element.scrollWidth > element.clientWidth;
}
};

判断用户是否滚动到底部



本质上就是当元素出现滚动条时,判断当前元素出现的高度 + 滚动条高度 = 元素本身的高度(包含隐藏部分)



element.scrollHeight - element.scrollTop === element.clientHeight

client


MouseEvent.clientX/Y


MounseEvent.clientX/Y同样也是只读属性,它提供事件发生时的应用客户端区域的水平坐标。



例如,不论页面是否有垂直/水平滚动,当你点击客户端区域的左上角时,鼠标事件的 clientX/Y 值都将为 0 。



其实MouseEvent.clientX/Y也就是相对于当前视口(浏览器可视区)进行位置计算。


转载一张非常直白的图:


clientX


Element.clientHeight/clientWidth


Element.clientWidth/clinetHeight 属性表示元素的内部宽度,以像素计。该属性包括内边距 padding,但不包括边框 border、外边距 margin 和垂直滚动条(如果有的话)。



内联元素以及没有 CSS 样式的元素的 clientWidth 属性值为 0。



在不出现滚动条时候Element.clientWidth/Height === Element.scrollWidth/Height


image.png


Element.clientTop/clientLeft


Element.clientLeft表示一个元素的左边框的宽度,以像素表示。如果元素的文本方向是从右向左(RTL, right-to-left),并且由于内容溢出导致左边出现了一个垂直滚动条,则该属性包括滚动条的宽度。clientLeft 不包括左外边距和左内边距。clientLeft 是只读的。


同样的Element.clientTop表示元素上边框的宽度,也是一个只读属性。



这两个属性日常使用会比较少,但是也应该了解以避免搞混这些看似名称都类似的属性。



offset


MouseEvent.offsetX/offsetY


MouseEvent 接口的只读属性 offsetX/Y 规定了事件对象与目标节点的内填充边(padding edge)在 X/Y 轴方向上的偏移量。


相信使用过offest的同学对这个属性深有体会,它是相对于父元素的左边/上方的偏移量。



注意是触发元素也就是 e.target,额外小心如果事件对象中存在从一个子元素当移动到子元素内部时,e.offsetX/Y 此时相对于子元素的左上角偏移量。



offsetWidth/offsetHeight


HTMLElement.offsetWidth/Height 是一个只读属性,返回一个元素的布局宽度/高度。


所谓的布局宽度也就是相对于我们上边说到的clientHeight/Width,offsetHeight/Width,他们都是不包含border以及滚动条的宽/高(如果存在的话)。


offsetWidth/offsetHeight返回元素的布局宽度/高度,包含元素的边框(border)、水平线/垂直线上的内边距(padding)、竖直/水平方向滚动条(scrollbar)(如果存在的话)、以及CSS设置的宽度(width)的值


offsetTop/left


HTMLElement.offsetLeft 是一个只读属性,返回当前元素左上角相对于 HTMLElement.offsetParent 节点的左边界偏移的像素值。



注意返回的是相对于 HTMLElement.offsetParent 节点左边边界的偏移量。



何为HTMLElement.offsetParent?



HTMLElement.offsetParent 是一个只读属性,返回一个指向最近的(指包含层级上的最近)包含该元素的定位元素或者最近的 table,td,th,body 元素。当元素的 style.display 设置为 "none" 时,offsetParent 返回 null。offsetParent 很有用,因为 offsetTop 和 offsetLeft 都是相对于其内边距边界的。 -- MDN



讲讲人话,当前元素的祖先组件节点如果不存在任何 table,td,th 以及 position 属性为 relative,absolute 等为定位元素时,offsetLeft/offsetTop 返回的是距离 body 左/上角的偏移量。


当祖先元素中有定位元素(或者上述标签元素)时,它就可以被称为元素的offsetParent。元素的 offsetLeft/offsetTop 的值等于它的左边框左侧/顶边框顶部到它的 offsetParent 元素左边框的距离。


我们来看看这张图:


image.png


计算元素距离 body 的偏移量


当我们需要获得元素距离 body 的距离时,但是又无法确定父元素是否存在定位元素时(大多数时候在组件开发中,并不清楚父节点是否存在定位)。此时需要实现类似 jqery 的 offset()方法:获得当前元素对于 body 的偏移量。



  • 无法直接使用 offsetLeft/offsetTop 获取,因为并不确定父元素是否存在定位元素。
  • 使用递归解决,累加偏移量 offset,当前 offsetParent 不为 body 时。
  • 继续递归向上超着 offsetParent 累加 offset,直到遇到 body 元素停止。

const getOffsetSize = function(Node: any, offset?: any): any {
if (!offset) {
offset = {
x: 0,
y: 0
};
}
if (Node === document.body) return offset;
offset.x = offset.x + Node.offsetLeft;
offset.y = offset.y + Node.offsetTop;
return getOffsetSize(Node.offsetParent, offset);
};


注意:这里不可以使用 parentNode 上文已经讲过 offsetLeft/top 针对的是 HTMLElement.offsetParent 的偏移量而非 parentNode 的偏移量。



Element.getBoundingClientRect


用法讲解


Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。



element.getBoundingClientRect()返回的相对于视口左上角的位置。



element.getBoundingClientRect()返回的 heightwidth 是针对元素可见区域的宽和高(具体尺寸根据 box-sizing 决定),并不包含滚动条被隐藏的内容。



TIP: 如果是标准盒子模型,元素的尺寸等于 width/height + padding + border-width 的总和。如果 box-sizing: border-box,元素的的尺寸等于 width/height。



rectObject = object.getBoundingClientRect();

返回值是一个 DOMRect 对象,这个对象是由该元素的 getClientRects() 方法返回的一组矩形的集合,就是该元素的 CSS 边框大小。返回的结果是包含完整元素的最小矩形,并且拥有 left, top, right, bottom, x, y, width, 和 height 这几个以像素为单位的只读属性用于描述整个边框。除了 widthheight 以外的属性是相对于视图窗口的左上角来计算的。


widthheight是计算元素的大小,其他属性都是相对于视口左上角来说的。


当计算边界矩形时,会考虑视口区域(或其他可滚动元素)内的滚动操作,也就是说,当滚动位置发生了改变,top 和 left 属性值就会随之立即发生变化(因此,它们的值是相对于视口的,而不是绝对的) 。如果你需要获得相对于整个网页左上角定位的属性值,那么只要给 top、left 属性值加上当前的滚动位置(通过 window.scrollX 和 window.scrollY),这样就可以获取与当前的滚动位置无关的值。


image.png


计算元素是否出现在视口内


利用的还是元素距离视口的位置小于视口的大小。



注意即便变成了负值,那么也表示元素曾经出现过在屏幕中只是现在不显示了而已。(就比如滑动过)



vue-lazy图片懒加载库源码就是这么判断的。


 isInView (): boolean {
const rect = this.el.getBoundingClientRect()
return rect.top < window.innerHeight && rect.left < window.innerWidth
}


如果rect.top < window.innerHeight表示当前元素已经已经出现在(过)页面中,left同理。



window.getComputedStyle


用法讲解


Window.getComputedStyle()方法返回一个对象,该对象在应用活动样式表并解析这些值可能包含的任何基本计算后报告元素的所有CSS属性的值。 私有的CSS属性值可以通过对象提供的API或通过简单地使用CSS属性名称进行索引来访问。


let style = window.getComputedStyle(element, [pseudoElt]);



  • element


     用于获取计算样式的Element




  • pseudoElt 可选


    指定一个要匹配的伪元素的字符串。必须对普通元素省略(或null)。




返回的style是一个实时的 CSSStyleDeclaration 对象,当元素的样式更改时,它会自动更新本身。


作者:19组清风
链接:https://juejin.cn/post/7006878952736161829

0 个评论

要回复文章请先登录注册