最近忽然看到了有关图片懒加载的问题,大体意思就是初始状态下页面只加载浏览器可视区域的图片,剩余图片在当浏览器可视区域滚动到其位置时才开始加载。貌似如今许多大型网站都有实现懒加载,因此我便就此问题思考了一下。首先第一个问题是浏览器没有相关的 API 方法能够检测某个元素是否在可视区域,那么就只能咱们人工计算,因此这里就涉及到了元素长宽,滚动条位置的知识。本文涉及的到的知识有元素长宽 clientWidth/offsetWidth/scrollWidth 的区别、以及 clientTop/offsetTop/scrollTop 的区别,并给了获取元素坐标的源代码。javascript
一般你们获取元素的长宽的时候都会使用一些框架封装好的方法,好比 jQuery.prototype.width() ,这些框架使用起来方便快捷,不过其中涉及到的知识仍是很是多的,关于元素的长宽,有多种的获取方法,其表明的实际意义也是不一样的。html
简单来讲能够使用下列公式:java
clientWidth = width(可视区) + paddingnode
offsetWidth = width(可视区) + padding + border浏览器
scrollWidth = width(内容区) 缓存
假设有咱们如下一个元素:框架
1 #test { 2 width: 100px; 3 height: 100px; 4 margin: 10px; 5 border: 10px solid #293482; 6 padding: 10px; 7 background-color: yellow; 8 overflow: auto; 9 }
clientWidth | offsetWidth | scrollWidth |
![]() |
![]() |
![]() |
![]() |
以上 DEMO 是常规状况下的区别,下面加上一个滚动条咱们们再来观察如下:函数
clientWidth | offsetWidth | scrollWidth |
![]() 注意这里不包括滚动条的长度工具 |
![]() |
![]() 这里实际上至关于内容的宽度网站
|
![]() |
咱们使用如下公式:
clientTop = border
offsetTop = 元素边框外围至父元素边框内围
scrollTop = 元素可视区域顶部至实际内容区域的顶部
给定如下两个元素 container 和 test
1 #container { 2 background-color: #F08D8D; 3 padding: 10px; 4 } 5 #test { 6 position: relative; 7 top: 10px; 8 width: 100px; 9 height: 100px; 10 margin: 20px; 11 border: 15px solid #293482; 12 padding: 10px; 13 background-color: yellow; 14 }
clientTop | offsetTop | scrollTop |
![]() |
![]() |
![]() |
![]() |
有了以上知识基础以后,咱们如今须要考虑的问题是,如何获取页面元素的绝对位置,也就是在文档流内容区的位置。咱们知道,元素的 offsetTop 属性能够获取当前元素边框外围至父元素边框内围的的距离,clientTop 能够获取元素边框的宽度。那么如今用一个递归的公式就能够求得当前元素在页面中的绝对位置:
Element.absoluteTop = Element.parent.absoluteTop + Element.offsetTop + Element.clientTop;
同理,咱们用参照元素的长宽减去 left 和 top 和定位,便可获得 right 和 bottom 的定位;
因此咱们能够编写如下工具来获取元素的绝对位置,也就是在内容区的定位(参照元素必须是目标元素的祖先元素):
1 var Position = {}; 2 (function () { 3 Position.getAbsolute = function (reference, target) { 4 //由于咱们会将目标元素的边框归入递归公式中,这里先减去对应的值 5 var result = { 6 left: -target.clientLeft, 7 top: -target.clientTop 8 } 9 var node = target; 10 while(node != reference && node != document){ 11 result.left = result.left + node.offsetLeft + node.clientLeft; 12 result.top = result.top + node.offsetTop + node.clientTop; 13 node = node.parentNode; 14 } 15 if(isNaN(reference.scrollLeft)){ 16 result.right = document.documentElement.scrollWidth - result.left; 17 result.bottom = document.documentElement.scrollHeight - result.top; 18 }else { 19 result.right = reference.scrollWidth - result.left; 20 result.bottom = reference.scrollHeight - result.top; 21 } 22 return result; 23 } 24 })();
此方法能够获取一个元素相对于一个父元素的定位,若是要获取元素在整张页面,直接传入 document 便可:
1 Position.getAbsolute(document, targetNode); //{left: left, right: right, top: top, bottom: bottom}
在上一小节中,咱们封装了一个函数,这个函数能够用来获取一个元素的相对于一个祖先元素的绝对定位坐标,在这一小节中,咱们来获取元素相对于浏览器窗口可视区域的定位坐标。在上一个函数中,咱们能够获取一个元素在 document 当中的定位,还记得咱们在第二小节中的 scrollTop 属性吗?该属性能够获取滚动窗口可视区域顶端距离内容区顶端的距离,咱们用元素的绝对定位坐标减去 document 的滚动定位就是咱们想要的浏览器窗口定位啦(相对于浏览器左上角):
ViewportTop = Element.absoluteTop - document.body.scrollTop;
这里须要注意一个兼容性的问题,在 Chrome 中能够用 document.body.scrollTop 和 window.pageYOffset,IE 7/8 只能经过 document.documentElement.scrollTop 获取, FireFox 和 IE9+ 能够用 document.documentElement.scrollTop 和 window.pageYOffset 获取,Safari 须要 window.pageYOffset 获取。因此这里咱们须要作一下浏览器兼容:
scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop;
注意这里的顺序,在 IE7/8 中 window.pageYOffset 是 undefined ,document.body.scrollTop 在任何浏览器中都有,只是不支持的值为 0,若是表达式返回 undefined ,会影响后面的计算操做。而 || 运算符是一个短路取真运算符,因此咱们要全部浏览器都有的 document.body.scrollTop 方法放在最后,关于 || 运算符的问题,能够参考 《探寻 JavaScript 逻辑运算符(与、或)的真谛》。
咱们在刚才的工具上添加一个方法:
1 var Position = {}; 2 (function () { 3 Position.getAbsolute = function (reference, target) { 4 var result = { 5 left: -target.clientLeft, 6 top: -target.clientTop 7 } 8 var node = target; 9 while(node != reference && node != document){ 10 result.left = result.left + node.offsetLeft + node.clientLeft; 11 result.top = result.top + node.offsetTop + node.clientTop; 12 node = node.parentNode; 13 } 14 if(isNaN(reference.scrollLeft)){ 15 result.right = document.documentElement.scrollWidth - result.left; 16 result.bottom = document.documentElement.scrollHeight - result.top; 17 }else { 18 result.right = reference.scrollWidth - result.left; 19 result.bottom = reference.scrollHeight - result.top; 20 } 21 return result; 22 } 23 Position.getViewport = function (target) { 24 var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; 25 var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; 26 var absolutePosi = this.getAbsolute(document, target); 27 var Viewport = { 28 left: absolutePosi.left - scrollLeft, 29 top: absolutePosi.top - scrollTop, 30 } 31 return Viewport; 32 } 33 })();
经过 Position.getViewport 方法能够获取元素相对于浏览器窗口的定位:
1 Postion.getViewport(targetNode); //{left: left, top: top}
在上面的几个方法中,咱们能够获取元素的文档流定位和视窗定位,不过这仍是不能判断一个元素是否在可视区域内,由于视窗定位能够是很是大的数字,这样元素就在视窗的后面。这里咱们须要使用浏览器视窗高度 window.innerHeight 属性,在 IE8 如下须要用 document.documentElement.clientHeight 来获取。
windowHeight = window.innerHeight || document.documentElement.clientHeight;
如今,咱们用窗口的高度,减去相对于浏览器窗口的定位,便可获取相对于浏览器窗口右下角的定位;
1 var Position = {}; 2 (function () { 3 Position.getAbsolute = function (reference, target) { 4 var result = { 5 left: -target.clientLeft, 6 top: -target.clientTop 7 } 8 var node = target; 9 while(node != reference && node != document){ 10 result.left = result.left + node.offsetLeft + node.clientLeft; 11 result.top = result.top + node.offsetTop + node.clientTop; 12 node = node.parentNode; 13 } 14 if(isNaN(reference.scrollLeft)){ 15 result.right = document.documentElement.scrollWidth - result.left; 16 result.bottom = document.documentElement.scrollHeight - result.top; 17 }else { 18 result.right = reference.scrollWidth - result.left; 19 result.bottom = reference.scrollHeight - result.top; 20 } 21 return result; 22 } 23 Position.getViewport = function (target) { 24 var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; 25 var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; 26 var windowHeight = window.innerHeight || document.documentElement.offsetHeight; 27 var windowWidth = window.innerWidth || document.documentElement.offsetWidth; 28 var absolutePosi = this.getAbsolute(document, target); 29 var Viewport = { 30 left: absolutePosi.left - scrollLeft, 31 top: absolutePosi.top - scrollTop, 32 right: windowWidth - (absolutePosi.left - scrollLeft), 33 bottom: windowHeight - (absolutePosi.top - scrollTop) 34 } 35 return Viewport; 36 } 37 })();
如今咱们使用 Position.getViewport(targetNode) 方法能够获取元素左上角相对于窗口4个方向的定位:
1 Position.getViewport(targetNode); //{left: left, top: top, right: right, bottom: bottom}
有了这个方法,如今就能够真正的判断元素是否在可视区域内了:
1 var Position = {}; 2 (function () { 3 Position.getAbsolute = function (reference, target) { 4 //由于咱们会将目标元素的边框归入递归公式中,这里先减去对应的值 5 var result = { 6 left: -target.clientLeft, 7 top: -target.clientTop 8 } 9 var node = target; 10 while(node != reference && node != document){ 11 result.left = result.left + node.offsetLeft + node.clientLeft; 12 result.top = result.top + node.offsetTop + node.clientTop; 13 node = node.parentNode; 14 } 15 if(isNaN(reference.scrollLeft)){ 16 result.right = document.documentElement.scrollWidth - result.left; 17 result.bottom = document.documentElement.scrollHeight - result.top; 18 }else { 19 result.right = reference.scrollWidth - result.left; 20 result.bottom = reference.scrollHeight - result.top; 21 } 22 return result; 23 } 24 Position.getViewport = function (target) { 25 var scrollTop = window.pageYOffset || document.documentElement.scrollTop || document.body.scrollTop; 26 var scrollLeft = window.pageXOffset || document.documentElement.scrollLeft || document.body.scrollLeft; 27 var windowHeight = window.innerHeight || document.documentElement.offsetHeight; 28 var windowWidth = window.innerWidth || document.documentElement.offsetWidth; 29 var absolutePosi = this.getAbsolute(document, target); 30 var Viewport = { 31 left: absolutePosi.left - scrollLeft, 32 top: absolutePosi.top - scrollTop, 33 right: windowWidth - (absolutePosi.left - scrollLeft), 34 bottom: windowHeight - (absolutePosi.top - scrollTop) 35 } 36 return Viewport; 37 } 38 Position.isViewport = function (target) { 39 var position = this.getViewport(target); 40 //这里须要加上元素自身的宽高,由于定位点是元素的左上角 41 if(position.left + target.offsetWidth < 0 || position.top + target.offsetHeight < 0){ 42 return false; 43 } 44 if(position.bottom < 0 || position.right < 0){ 45 return false; 46 } 47 return true; 48 } 49 })();
判断理由很简单,若是有一边的定位是负值,那么元素就不在视窗内。
1 Position.getAbsolute(document, targetNode); //获取元素在文档流中的绝对坐标 2 Position.getViewport(targetNode); //获取元素相对于浏览器视窗的坐标 3 Position.isViewport(targetNode); //判断元素是否在浏览器视窗内
浏览器兼容性:
Chrome | FireFox | IE | Safari | Edge |
Support | Support | IE8+ Support | Support | Support |
IE7 也能够使用,不过结果可能会有一点差别。
在文章的开始,咱们提到过图片懒加载的问题,那么具体须要怎么实现呢?这里只是给出一个思路:
初始状态下不设置 img 的 src,将图片的真实 url 缓存在 Img 标签上,咱们能够设置为 data-src ,这样图片就不会加载了,随后给鼠标添加 mousescroll 事情,每次鼠标滚动的时候将进入可视区域的图片的 src 还原,这样也就实现了图片懒加载效果。不过初始状态下须要将页面可视区域的图片先加载出来。
阮一峰 — 用 JavaScript 获取元素页面元素位置
张媛媛 — js实现一个图片懒加载插件