个性签名: 生如夏花,逝如冬雪;人生如此,何悔何怨。
前言: 经常需要计算元素的大小或者所在页面的位置,offsetWidth,clientWidth,scrollWidth,scrollTop这几个关键字的出现更是家常便饭,每次碰到都需要事先实验一番。为了下次开发提高效率。在这里一次性做个总结,以用来判断元素是否在可视区域以及用原生js简单实现懒加载。文末有个简单的懒加载实现的demo,有需要的可以看一下。
Ŀ¼
工欲善其事,必先利其器。在判断元素是否在可视区域实现简单的原生懒加载前,我们先简单回顾下以下几个关键的概念。
ps: 如果你对这些概念已经比较熟悉了,可以直接跳到第五点查看关键代码示例。
1. 偏移量
偏移量(offset dimension),元素的可见大小由其高度、宽度决定,包括所有内边距、滚动条和边框大小(注意,不包括外边距)。通过下列4个属性可以取得元素的偏移量。
偏移量 | 概念 | 公式 |
---|---|---|
offsetHeight | 元素在垂直方向上占用的空间大小,以像素计。包括元素的高度、(可见的) 水平滚动条的高度、上边框高度和下边框高度。 | offsetHeght = content + padding + border + scrollX |
offsetWidth | 元素在水平方向上占用的空间大小,以像素计。包括元素的宽度、(可见的)垂 直滚动条的宽度、左边框宽度和右边框宽度。 | offsetWidth = content + padding + border + scrollY |
offsetLeft | 元素的左外边框至**包含元素的左内边框之间的像素距离。 | |
offsetTop | 元素的上外边框至包含元素的上内边框之间的像素距离。 |
其中,offsetLeft 和 offsetTop 属性与包含元素有关,包含元素的引用保存在 offsetParent 属性中。**offsetParent 属性不一定与 parentNode 的值相等。
如下图显示
注意: 所有这些偏移量属性都是只读的,而且每次访问它们都需要重新计算。因此,应该尽量避免重复访问这些属性;如果需要重复使用其中某些属性的值,可以将它们保 存在局部变量中,以提高性能。
这也是上篇文章文字跑马灯项目中(戳此跳转),为什么增加padding后,textWidth需要重新获取的原因
小结
偏移量: 只读属性;包括滚动条和边框,不包括外边距。
2. 客户区大小
客户区大小是只读的,每次访问都要重新计算的。
客户区大小 | 概念 | 公式 |
---|---|---|
clientWidth | clientWidth 属性是元素内容区宽度加 上左右内边距宽度; | clientWidth = content + padding |
clientHeight | 元素内容区高度加上上下内边距高度 | clientHeight = content + padding |
最常用到这些属性的情况,就是确定浏览器视口大小的时候(在 IE7 之前的版本中)。如下面的例子所示:
function getViewport(){ // 检查 document.compatMode 属性,以确定浏览器是否运行在混杂模式。 // Safari3.1 之前的版本不支持这个属性,因此就会自动执行 else 语句 if (document.compatMode == "BackCompat"){ return { width: document.body.clientWidth, height: document.body.clientHeight }; } else { return { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }; } }
小结
客户区大小: 只读属性;不包括滚动条和边框,不包括外边距。
3. 滚动大小
概念 | |
---|---|
scrollHeight | 在没有滚动条的情况下,元素内容的总高度。 |
scrollWidth | 在没有滚动条的情况下,元素内容的总宽度。 |
scrollLeft | 被隐藏在内容区域左侧的像素数。通过设置这个属性可以改变元素的滚动位置。 |
scrollTop | 被隐藏在内容区域上方的像素数。通过设置这个属性可以改变元素的滚动位置。 |
scrollWidth 和 scrollHeight 主要用于确定元素内容的实际大小。
scrollLeft 和 scrollTop属性既可以确定元素当前滚动的状态,也可以设置元素的滚动位 置。在元素尚未被滚动时,这两个属性的值都等于 0。如果元素被垂直滚动了,那么 scrollTop 的值 会大于 0,且表示元素上方不可见内容的像素高度。如果元素被水平滚动了,那么 scrollLeft 的值会 大于 0,且表示元素左侧不可见内容的像素宽度。这两个属性都是可以设置的,因此将元素的 scrollLeft 和 scrollTop 设置为 0,就可以重置元素的滚动位置。比如:上篇文章文字跑马灯项目中scrollLeft的使用(戳此跳转)
小结
只读属性,不包括滚动条、border。
4. 确定元素大小
getBoundingClientRect
getBoundingClientRect的兼容性写法:
对于不支持 getBoundingClientRect()的浏览器,可以通过其他手段取得相同的信息。一般来 说,right 和 left 的差值与 offsetWidth 的值相等,而 bottom 和 top 的差值与 offsetHeight 相等。综合上述,就可以创建出下面这个跨浏览器的函数:
function getElementLeft(element){ var actualLeft = element.offsetLeft; var current = element.offsetParent; while (current !== null){ actualLeft += current.offsetLeft; current = current.offsetParent; } return actualLeft; } function getElementTop(element){ var actualTop = element.offsetTop; var current = element.offsetParent; while (current !== null){ actualTop += current. offsetTop; current = current.offsetParent; } return actualTop; } function getBoundingClientRect(element) { var scrollTop = document.documentElement.scrollTop; var scrollLeft = document.documentElement.scrollLeft; if (element.getBoundingClientRect) { if (typeof arguments.callee.offset != "number") { var temp = document.createElement("div"); temp.style.cssText = "position:absolute;left:0;top:0;"; document.body.appendChild(temp); arguments.callee.offset = -temp.getBoundingClientRect().top - scrollTop; document.body.removeChild(temp); temp = null; } var rect = element.getBoundingClientRect(); var offset = arguments.callee.offset; return { left: rect.left + offset, right: rect.right + offset, top: rect.top + offset, bottom: rect.bottom + offset }; } else { var actualLeft = getElementLeft(element); var actualTop = getElementTop(element); return { left: actualLeft - scrollLeft, right: actualLeft + element.offsetWidth - scrollLeft, top: actualTop - scrollTop, bottom: actualTop + element.offsetHeight - scrollTop } } }
5.判断元素是否在可视区域
知道了元素的大小以及所位于的区域外,我们可以做些什么呢?我们可以通过上面学到的知识点来检测元素是否在可视区域,再说大一点,这也是懒加载图片的实现原理。
5.1 第一种方法
公式: el.offsetTop - document.documentElement.scrollTop <= viewPortHeight
function isInViewPortOfOne (el) { // viewPortHeight 兼容所有浏览器写法 const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight const offsetTop = el.offsetTop const scrollTop = document.documentElement.scrollTop const top = offsetTop - scrollTop console.log('top', top) // 这里有个+100是为了提前加载+ 100 return top <= viewPortHeight + 100 }
5.2 第二种方法
公式: el.getBoundingClientReact().top <= viewPortHeight
其实, el.offsetTop - document.documentElement.scrollTop = el.getBoundingClientRect().top, 利用这点,我们可以用下面代码代替方法一
function isInViewPortOfTwo (el) { const viewPortHeight = window.innerHeight || document.documentElement.clientHeight || document.body.clientHeight const top = el.getBoundingClientRect() && el.getBoundingClientRect().top console.log('top', top) return top <= viewPortHeight + 100 }
5.3 第三种方法
公式: intersectionRatio > 0 && intersectionRatio <= 1
// 定义一个交叉观察器 const io = new IntersectionObserver(ioes => { ioes.forEach(ioe => { const el = ioe.target const intersectionRatio = ioe.intersectionRatio if (intersectionRatio > 0 && intersectionRatio <= 1) { loadImg(el) io.unobserve(el) } el.onload = el.onerror = () => io.unobserve(el) }) }) // 执行交叉观察器 function isInViewPortOfThree (el) { io.observe(el) }
5.4 兼容性比较
在兼容性方面,我们知道越原始的方法兼容性是最好的,那么第二种方法和第三种方法能否代替第三种方法呢?我们来看看。
从caniuse的数据来看,getBoundingClientReact的适配情况很乐观了。
所以,如果在移动端和桌面端都要做兼容适配的话,方法二完全可以代替方法一进行适配了。如果仅仅是桌面端适配(比如运营后台),我们或许可以尝试下新的IntersectionObserver方法,毕竟IntersectionObserver里面还有更多丰富的功能等着我们去体验呢。
5.5 实例
有时,我们希望某些静态资源(比如图片),只有用户向下滚动,它们进入视口时才加载,这样可以节省带宽,提高网页性能。这就叫做"惰性加载",也称为懒加载。
惰性加载预览DEMO(放入你的本地图片即可通过更换不同方法实现懒加载)
------------------------------ 华丽的分割线 ---------------------------
ps: 有公众号大佬想减轻日常写推文的时间消耗,收购技术原创文章的稿子放在自己公众号展示推送的,可以联系找我合作哈。(在公众号里可以查到我的联系方式)
关于我
参考
- 原生JS实现最简单的图片懒加载
- JavaScript高级程序设计
- IntersectionObserver API 使用教程