一、背景
图片是非常占用页面渲染时间的,尤其是一些图片比较多的页面,过多的图片可能会造成页面的卡顿,降低流畅度影响用户体验,我们在实际开发中,对于处于视口外的图片,在用户没有滚动到位置的时候没必要渲染,此时我们就需要用到懒加载,让图片延后渲染。
vue中有一个插件vue-lazyload,它提供了一个“vue指令”可以完成上面需求,那他的原理是什么呢,最近我看了一下这个库的源代码,跟大家分享一下。
插件源码我就不贴了哈,为了通用性,它里面有很多处理通用性代码,我只拿最基本的代码出来。
全量代码我放在了文章结尾,各位同学可以手动复制看效果哦
二、效果
说明一下,为了效果明显,我把控制渲染的逻辑提前了一下
这样能明显的看到效果,实际开发中,应当把,加载新图片的逻辑放在看不到的位置进行提前加载。
三、一句话原理
获取视口的底部位置,并把所有img的src属性 复制给data-src,使用数组收集所有的img dom 对象。通过监听页面的scroll事件,使用getBoundingClientRect()方法,判断需要加载图片的img标签的top值是否在视口内,如果在视口内,则把当前img标签的data-src复制给src,进行加载。
名词解释
- img html用来加载图片的一个标签
- src img指向图片位置的属性
- data-* 这个是一个自定义属性,可以在dom节点中自定义属性,后面可以通过元素的dataset属性获取,比如本文中用到的data-src,后面就可以使用 dom.dataset.src获取。
- getBoundingClientRect() 获取,元素渲染后,在页面所处的位置
原理真的是非常的简单呢,涉及到的知识点也不多,接下来我会详细说一下原理,并跟大家一起实现一下这个图片懒加载。
四、实现原理
一、获取视口底部位置,在移动端可以使用 clientHeight 来获取到视口底部的像素值,pc端可以使用getBoundingClientRect() 获取
二、收集所有img标签转成数组并保存,当然实际开发中,我们对获取方式还要加一些其他条件,避免有些不需要懒加载的图片也被收集进来,这里我就简单点了。
三、写个渲染函数用来处理图片的渲染逻辑,主要逻辑是判断当前dom节点是否在视口内,需要注意的是有个问题:
- 开始的时候由于dom节点没有图片,宽度没有被撑开,导致所有节点都在视口内
- 由于问题"1"的存在,我们需要把排在前面的dom渲染完,才能判断下一dom是否在视口内
- 可以使用img的load事件的回调来处理。
五、代码实现与讲解
全量代码我放在了文章结尾,给位同学可以手动复制看效果哦,图片我没有提供,各位可以自己找一下哈。
一、基本结构和样式
html
<div id="mobile-viewport">
<img data-src="./pic/1.jpeg">
<img data-src="./pic/2.jpeg">
<img data-src="./pic/3.jpeg">
<img data-src="./pic/4.jpeg">
<img data-src="./pic/5.jpeg">
<img data-src="./pic/6.jpeg">
</div>
需要注意的是,这里用的不是src,是data-src
css
#mobile-viewport {
height: 480px;
width: 270px;
border: 2px solid #000000;
overflow: auto;
}
img {
width: 100%;
}
二、获取视口底部位置
//模拟移动端视口 移动端使用 clientHeight
let mobileViewportDom = document.getElementById('mobile-viewport')
let mobileViewportRect = mobileViewportDom.getBoundingClientRect()
//获得视口最下端的位置
let { bottom : mobileViewportRectBottom } = mobileViewportRect
三、获取所有img节点
//取到所有img
let domArray = Array.from(document.getElementsByTagName('img'))
四、渲染函数
//定义渲染方法
function renderPicture() {
//如果 img 数组为空直接返回
if (domArray.length === 0 ) {
return
}
//获取当前长度,后面对比用
let beginL = domArray.length
//取数组第一个
let imgDom = domArray[0]
//给当前dom 添加load回调
function isRenderPicture(){
//回调中对比length是否发生改变
if(beginL !== domArray.length){
// 如果发生改变,在调用一次看下一个是否也需要渲染
renderPicture(domArray)
}
imgDom.removeEventListener("load", isRenderPicture)
}
//给当前pic dom 添加加载回调
imgDom.addEventListener('load', isRenderPicture)
//获取当前img dom在页面中的位置
let domRact = imgDom.getBoundingClientRect()
//用img的最上点位置与视口的最下点对比
if(domRact.top < mobileViewportRectBottom){
//删除数组第一个,此处domArray的length发生改变
domArray.shift()
//复制scr属性
imgDom.src = imgDom.dataset.src
}
}
有几个比较复杂的点:
1、在判断渲染点的时候,如果进行了渲染,我们会把已经渲染的dom节点移除domArray数组,所以在load事件中我们需要判断一下,两次domArray的长度是否相等,如果不等,证明渲染过,此时我们还要在调用一下渲染函数,判断下一个dom节点是否也需要渲染。
2、在添加load事件的时候,要在load的回调执行完移除这个监听,否则会非常的影响性能。
3、使用了shift(),函数应用了”队列“这个数据结构的特性,不熟悉的同学稍微看一下数据结构与算法吧。
六、总结
好的,到此我们这个功能就简单的实现了,这里给大家留两个思考题
一、代码中我是用了”domRact.top < mobileViewportRectBottom“ 来判断是否需要渲染,如何修改这个判断让图片渲染提前呢?
二、能不能利用浏览器的空闲时间,比如用户在浏览某个商品,把图片预载到内存中,在滚动的时候直接从内存加载呢?如何实现呢?
欢迎在评论区留言,一起探讨
录:全量代码
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片懒加载的核心原理</title>
</head>
<style>
#mobile-viewport {
height: 480px;
width: 270px;
border: 2px solid #000000;
overflow: auto;
}
img {
width: 100%;
}
</style>
<body>
<div id="mobile-viewport">
<img data-src="./pic/1.jpeg">
<img data-src="./pic/2.jpeg">
<img data-src="./pic/3.jpeg">
<img data-src="./pic/4.jpeg">
<img data-src="./pic/5.jpeg">
<img data-src="./pic/6.jpeg">
</div>
<script src="../utils/lodsh.js"></script>
<script>
//模拟移动端视口 移动端使用 clientHeight
let mobileViewportDom = document.getElementById('mobile-viewport')
let mobileViewportRect = mobileViewportDom.getBoundingClientRect()
//获得视口最下端的位置
let { bottom : mobileViewportRectBottom } = mobileViewportRect
//取到所有img
let domArray = Array.from(document.getElementsByTagName('img'))
//定义渲染方法
function renderPicture() {
//如果 img 数组为空直接返回
if (domArray.length === 0 ) {
return
}
//获取当前长度,后面对比用
let beginL = domArray.length
//取数组第一个
let imgDom = domArray[0]
//给当前dom 添加load回调
function isRenderPicture(){
//回调中对比length是否发生改变
if(beginL !== domArray.length){
// 如果发生改变,在调用一次看下一个是否也需要渲染
renderPicture(domArray)
}
imgDom.removeEventListener("load", isRenderPicture)
}
//给当前pic dom 添加加载回调
imgDom.addEventListener('load', isRenderPicture)
//获取当前img dom在页面中的位置
let domRact = imgDom.getBoundingClientRect()
//用img的最上点位置与视口的最下点对比
if(domRact.top < mobileViewportRectBottom){
//删除数组第一个,此处domArray的length发生改变
domArray.shift()
//复制scr属性
imgDom.src = imgDom.dataset.src
}
}
//添加滚动事件
mobileViewportDom.addEventListener('scroll', _.debounce(renderPicture,200))
renderPicture(domArray)
</script>
</body>
</html>