哈喽哇,大家好.经常我们可能会有需求,需要做图片的懒加载需求。顾名思义,懒加载就是先不加载图片资源,等需要用到或者浏览到的时候再加载出来。
这里我总结了三种实现方法。
1. 分页拉取图片资源.
2. 传统的手动实现判断图片是否进入视口
3. 使用IntersectionObserver Api进行实现(推荐)
这里我使用了Vue3进行演示。代码复制可用!让我们开始吧。
一、分页拉取数据
这里我使用canvas来绘制,提高性能
首选需要给滚动容器加上'scroll'滚动事件,用来监听当前屏幕浏览的位置
const handleScroll = (e: Event) => {
const box = e.target as HTMLDivElement
if (box.scrollHeight - box.scrollTop - box.clientHeight < 100) {
console.log('scroll')
loadImgs()
}
}
这里去计算了当滚动容器滚动到不足100像素的时候就去加载更多的图片(滚动容器总高度 - 滚动的距离-容器的视口高度)
const loadImgs = () => {
if (hasMorePicture.value) {
const data = images.sliceImg(currentIndex.value, loadCount.value)//或者请求后端图片路径数据
if (!data || data.length === 0) {
hasMorePicture.value = false
}
initData(data)
currentIndex.value += data.length
}
}
这里我们需要使用一个变量hasMorePicture来标记是否还有更多的图片,因为用户在使用滚动时不一定是一直往下滚动的所以会多次触发加载图片,因此需要进行一个判断.
因为我使用canvas来绘制使用了canvas的drawImg方法以下是该方法的具体属性值。
drawImage(image, dx, dy)
drawImage(image, dx, dy, dWidth, dHeight)
drawImage(image, sx, sy, sWidth, sHeight, dx, dy, dWidth, dHeight)
第一个参数为HTMLImageElement也就是加载的图片
/**
const loadImage = (src: string): Promise<HTMLImageElement> => {
return new Promise((resolve, reject) => {
const img = new Image()
img.src = src
img.onload = () => resolve(img)
img.onerror = reject
})
}
*/
dx,源 image 的左上角在目标画布上 X 轴坐标
dy,源 image 的左上角在目标画布上 Y 轴坐标。
sx,裁剪图片起始的x坐标
sy,裁剪图片起始的y坐标
dWidth,绘制的图片宽度,
dHeight,绘制的图片高度
sWidth,裁剪图片起始的宽度
sHeight,裁剪图片起始的高度
这里因为我需要实现瀑布流效果所以需要计算每个图片的宽度高度以及在画布的x坐标和y坐标
const initData = async (src: string[]) => {
for (const img of src) {
const sy = await loadImage(img) //加载图片
const cxp = sy.width / imgWidth.value //计算与默认宽度的比例
const imgHeight = sy.height / cxp //计算比例后的高度
const min = Math.min(...columnHeights.value) //从每一行中选取最短的位置放置图片
const index = columnHeights.value.indexOf(min) //获取索引
const x = (index % currentColum.value) * (imgWidth.value + currentGap.value) //计算在画布的x位置
const y = columnHeights.value[index] + currentGap.value ////计算在画布的y位置
columnHeights.value[index % currentColum.value] += imgHeight + currentGap.value //更新高度位置数组
imgList.value.push({ src: sy, x, y, width: imgWidth.value, height: imgHeight }) //加入绘制数组
}
if (checkAndReSetCanvas()) { //当高度位置数组里面最大的高度大于canvas画布高度时需要重新设置canvas高度
SetCanvasSize()
}
}
计算是否重新设置画布
function checkAndReSetCanvas(): boolean {
const maxHeight = Math.max(...columnHeights.value)
return maxHeight > maxScreenHeight.value
}
在这里重新设置canvas 的画布大小并重新绘制瀑布流
const SetCanvasSize = () => {
canvasRef.value!.width = currentColum.value * imgWidth.value + (currentColum.value - 1) * currentGap.value
const maxHeight = Math.max(maxScreenHeight.value, ...columnHeights.value)
canvasRef.value!.height = maxHeight
maxScreenHeight.value = maxHeight
drawImg()
}
//绘制图片
const drawImg = async () => {
if (imgList.value && imgList.value.length > 0) {
for (const ss of imgList.value) {
ctx.value?.drawImage(ss.src as HTMLImageElement, ss.x, ss.y, ss.width, ss.height)
}
}
}
至此以及完成了分页加载图片资源并绘制到canvas的过程
二、传统的手动实现判断图片是否进入视口
传统的方式同样需要绑定滚动事件,当每次滚动时去判断当前的图片是否进入视口
这里每个图片的warpper使用 ’absolute‘ 绝对定位来方便设置每个图片的位置
const handleScroll = (e: Event) => {
if (!e.target) return
const scroll = (e.target as HTMLElement).scrollTop
toShowMore(scroll)
}
// 滚动(节流)
const toShowMore = doubouce((top: number) => toShowImg(top), 200)
由于scroll事件触发比较频繁,所以需要使用节流来节省性能
// ✅ 懒加载
const toShowImg = (scrollTop: number) => {
const viewHeight = boxRef.value!.clientHeight
for (const item of imgList.value) {
if (!item.loaded && (item.y - scrollTop) < viewHeight + 100) { //当(图片的纵坐标-滚动距离<视口高度)时就是需要即将进入页面要加载的图片
item.source = item.src //将有效的图片资源地址赋值过去
item.loaded = true //标记为已经加载
}
}
}
这样就实现了传统方式的图片懒加载
三、使用现代浏览器的IntersectionObserver api来实现(*推荐)
IntersectionObserver 是浏览器原生提供的一个可观察元素元素可见性变化的api,通俗来讲就是:
当元素进入或离开视口时,回调函数会被触发
优势:
不需要监听scroll事件
浏览器原生支持更好
可以做更多的配置
异步执行不阻塞主线程
当然除了图片懒加载它还可以做更多的事情,比如可以观察某个元素的位置,当它到达指定区域时触发动画等等,听起来是不是有点像gsap的ScrollTrigger插件,但是它可以做的远不止此。
下面请看使用示例
const initObserver = () => {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
if (!img.getAttribute('src')) {
img.src = img.dataset.src || '' //将暂存的图片赋值给src属性
}
observer.unobserve(img) //取消图片的观察
}
})
},
{
root: boxRef.value,//观察的容器如果为null,则根据视口进行观察
rootMargin: '100px', // 距离观察容器多远的距离触发回调
threshold: 0.1 // 观察阈值,0-1之间,表示在可视区域的比例,触发回调
}
)
imgRefs.value.forEach(img => {
observer.observe(img) //把图片加入观察
})
}
使用IntersectionObserver进行懒加载只需要做两件事,第一个就是要把需要进行懒加载的对象放到观察里面,第二件事就是配置回调,在回调里面有两部分,一个是observe的回调(如下图)这个会返回进入观察元素的dom,第二个就是观察的参数(具体如上代码注释)
记得触发回调时要取消观察哦,不然会一直触发回调
这样看使用起来是不是很简单
接着我们来看一下它有那些方法和参数配置
interface IntersectionObserver {
readonly root: Element | Document | null;
readonly rootMargin: string;
readonly thresholds: ReadonlyArray<number>;
disconnect(): void;
observe(target: Element): void;
takeRecords(): IntersectionObserverEntry[];
unobserve(target: Element): void;
}
root: boxRef.value,//观察的容器如果为null,则根据视口进行观察 rootMargin: '100px', // 距离观察容器多远的距离触发回调 threshold: 0.1 // 观察阈值,0-1之间,表示在可视区域的比例,触发回调
注意如果这里使用了rootMargin和threshold需要同时满足才会触发回调
比如需要满足进入视口100px以及0.1也就是10%的高度才会触发 observe回调
observe:observe()方法把元素加入观察
unobserve:unobserve()方法取消元素的观察
disconnect:调用disconnect()方法,停止监听所有的元素
takeRecords:获取还没处理的监听记录(很少用)
好了,我今天的分享就到这里了,这是我第一次发文章,以总结和学习,希望大家多多指教