图片懒加载,我总结了三个方式

哈喽哇,大家好.经常我们可能会有需求,需要做图片的懒加载需求。顾名思义,懒加载就是先不加载图片资源,等需要用到或者浏览到的时候再加载出来。

这里我总结了三种实现方法。

1. 分页拉取图片资源.

2. 传统的手动实现判断图片是否进入视口

3. 使用IntersectionObserver Api进行实现(推荐)

这里我使用了Vue3进行演示。代码复制可用!让我们开始吧。

一、分页拉取数据

这里我使用canvas来绘制,提高性能

首选需要给滚动容器加上'scroll'滚动事件,用来监听当前屏幕浏览的位置

ts 复制代码
const handleScroll = (e: Event) => {
    const box = e.target as HTMLDivElement
    if (box.scrollHeight - box.scrollTop - box.clientHeight < 100) {
        console.log('scroll')
        loadImgs()
    }
}

这里去计算了当滚动容器滚动到不足100像素的时候就去加载更多的图片(滚动容器总高度 - 滚动的距离-容器的视口高度)

ini 复制代码
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方法以下是该方法的具体属性值。

ini 复制代码
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坐标

php 复制代码
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()
    }
}

计算是否重新设置画布

javascript 复制代码
function checkAndReSetCanvas(): boolean {
    const maxHeight = Math.max(...columnHeights.value)
    return maxHeight > maxScreenHeight.value
}

在这里重新设置canvas 的画布大小并重新绘制瀑布流

ini 复制代码
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' 绝对定位来方便设置每个图片的位置

scss 复制代码
const handleScroll = (e: Event) => {
    if (!e.target) return
    const scroll = (e.target as HTMLElement).scrollTop
    toShowMore(scroll)
}
scss 复制代码
// 滚动(节流)
const toShowMore = doubouce((top: number) => toShowImg(top), 200)

由于scroll事件触发比较频繁,所以需要使用节流来节省性能

ts 复制代码
// ✅ 懒加载
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插件,但是它可以做的远不止此。

下面请看使用示例

javascript 复制代码
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,第二个就是观察的参数(具体如上代码注释)

记得触发回调时要取消观察哦,不然会一直触发回调

这样看使用起来是不是很简单

接着我们来看一下它有那些方法和参数配置

ts 复制代码
    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:获取还没处理的监听记录(很少用)

好了,我今天的分享就到这里了,这是我第一次发文章,以总结和学习,希望大家多多指教

相关推荐
ssshooter1 小时前
infer,TS 类型系统的手术刀
前端·面试·typescript
灰太狼大大王1 小时前
2026 前端基石:HTML5 全景知识体系指南(从入门到架构师思维)
前端
米丘1 小时前
vue-router 5.x 文件式路由
前端·vue.js
始持1 小时前
第十五讲 本地存储
前端·flutter
不甜情歌2 小时前
JS 拷贝:浅拷贝 / 深拷贝原理 + 常用方法
前端·javascript
敲代码的约德尔人2 小时前
Vue 3 响应式系统完全指南:我在 4 个项目中踩坑后总结的血泪经验
前端·vue.js
始持2 小时前
第十四讲 网络请求与数据解析
前端·flutter
Roselind_Yi2 小时前
技术拆解:《从音频到动效:我是如何用 Web Audio API 拆解音乐的?》
前端·javascript·人工智能·音视频·语音识别·实时音视频·audiolm
和科比合砍81分2 小时前
pnpm:public-hoist-pattern[]配置
前端