JS实现懒加载以及加载进度实现 - 手把手从页面到核心详解

目录

一、前言

二、前置准备

2-1、 页面布局 

三、核心方法 

3-1、传统实现(推荐,略微麻烦)

3-2、IntersectionObserver对象(超级推荐,但兼容性有待考证)

四、实时加载进度

4-1、进度条实现 

4-2、实时进度


先上全部效果视频一览到底:

20230623_181918

一、前言

前端懒加载技术对于现在来说已经是屡见不鲜,那我们需要知道在什么场景下需要用到懒加载,比如某宝,某东等首页展示,在其浏览商品的时候是可以无限滚动加载的,以及大量的图文展示,也需要用到懒加载,其目的就是为了减轻浏览器渲染的压力从而提升用户体验,按量渲染,而不是嗖的一下把压力全部给到浏览器

二、前置准备

2-1、 页面布局 

        页面布局采用flex弹性布局,模拟更加真实的场景,第一次写文章,由于直接粘贴代码还得调缩进,所以这里是省略了html、head等标签,实际使用是必须要加上的!

index.html

<style>
    * { 
        margin: 0; padding: 0 
    }
    body { 
        background: rgb(253, 253, 253) 
    }
    .container {
        padding: 1vw;
        display: flex;
        flex-wrap: wrap;
        gap: 1rem
    }
    .role-item {
        width: 15vw;
        height: 37vh;
        border: .05rem solid #ccc
    }
    .role-item > img {
        height: 30vh
    }
    .role-item > div {
        padding: 1rem
    }
    .role-item > div > span {
        font-size: 1rem;
        color: rgb(98, 98, 98)
    }
</style>
<body>
    <div id="app">
        <!-- 加载容器 -->
        <div class="container">
            <!-- 渲染区 -->
            <div class="role-item">
                <img src="./NTIyOTgwNTE2NzE4NTYyOTQyOF8xNjgxMjk2NzgzNTk0_7.png">
                <div>
                    <span>AI娃娃</span>
                </div>
            </div>

        </div>
    </div>
</body>

这里的图片是随便找的网络图片,如有侵权请联系立即删除,一个图最小也是900k,这将对懒加载的性能测试会更加直观,这里我们只放了一个看看样式

        

接下来放满一个屏幕看看flex布局有无生效 

 

 至此我们得到一个差不多完美的页面,效果测试完毕,我们删除掉写的静态标签,使用js动态渲染,先编写一个lazyloader.js文件并引入到页面

lazyloader.js

......
<script src="./lazyloader.js"></script>
......

大量的图片渲染正是我们需要用到懒加载优化的场景!接下来谈谈核心方法以及思路

三、核心方法 

3-1、传统实现(推荐,略微麻烦)

        这里浅浅的谈一谈传统实现方案,因为我们不是奔着监听鼠标事件去实现的,大多数开发者实现可能会借助onscroll以及onmousewheel鼠标滚动事件去监听网页被卷去的上半部分像素等方案,这里写一个简单的例子:

//监听滚动条事件 性能高于mousewheel
window.addEventListener('scroll', (evt) => { 
  console.log('scroll')
  console.log(evt)
})

//监听鼠标滚轮事件 能获取到的有用信息比scroll丰富
window.addEventListener('mousewheel', (evt) => { 
  console.log('mousewheel')
  console.log(evt)
})

我们看看他俩有什么不同,这是控制台打印的属性:

 上图我们可以看出scroll事件几乎是拿不到任何关于位置的信息,如top,offsetX之类的,再看看mousewheel事件能得到什么有用的

 

 mosewheel事件通过wheelDalta属性值判断是上滚(150)还是下滚(-150),间值是150,看到这里可能大家还是疑惑,那这些东西还是没什么用啊,坐标属性太多看的也很晕,还不是要自己写一个实现;确实如此,想单纯通过event事件提取的信息实现懒加载还是有点麻烦,那是怎么实现的,这里给大家提供两个关于网页坐标信息的方法

属性说明
document.documentElement.scrollTop获取指定DOM被卷去的上部分,单位px
document.documentElement.scrollLeft获取指定DOM被卷去的左部分

有了这些属性还不简单?直接上手,就用scroll事件改造一下,值得一提的是,document.body.scrollTop与docu.documentElement.scrollTop属性作用虽然相同,但在某些时刻,可能会出现获取到的像素一直为0的情况,原因是这两个属性一旦其中某一个属性有具体的数据之后,另一个就为恒为0,通俗来说就是只能“二选一”,一般是把这两个属性值加在一起计算,马上改造scroll方法试一试

//改造后
window.addEventListener('scroll', (evt) => { 
  console.log('scroll')
  console.log(document.documentElement.scrollTop)
})

我们滚动网页,前提是网页内容足够长,让我们可以滚动才行,控制台会打印出具体的像素

 有了如此详细的信息,那算法还不是手到擒来?!实现一个算法,当达到某个阈值后请求后端的接口渲染数据,具体实现就不再多说

分析:这两种事件有什么不同?mousewheel是鼠标滚轮事件,只要一滚动就会触发,所以性能上是差于scroll的,但scroll不同,scroll滚动条事件,是滚动条在滚动时才会触发,也就是说在滚动条到顶和到底是不会再次重复触发的

3-2、IntersectionObserver对象(超级推荐,但兼容性有待考证)

介绍:IntersectionObserver对象主要用户监听队列内的所有元素在窗口的出现情况,你可以把它看成一个DOM观察者,常用于判断DOM是否出现在窗口之中,也就是我们的浏览器窗口,这也是本人围绕这篇文章要讲解的核心API

 一、使用场景:

1、懒加载

2、出场动画

        我们在浏览一些官网时,应该深有感触,当滚动到下一个页面的过程中,一些DOM都是在边边角角向中心汇拢或以平滑的动画出现,有的是淡入淡出,有的是向上、中心慢慢归位,原理无非就是结合IntersectionObserver对class增删查改,你看不见它的时候,他本尊可能是乱作一团

二、使用方法:

        使用方法非常简单

//接收一个回调函数以及一个可选的配置项
const observer = new IntersectionObserver(callback[,option])

1、callback回调函数

        该回调函数有两个默认参数,第一个参数是一个需要监听的DOM(网页中的任意标签)队列,也就是一个数组,我们要监听的DOM对象全部放在此队列中,那我们该如何放置要进行监听的对象呢?通过该实例对象的observer方法可以向队列添加一个DOM

const observe = new IntersectionObserver((entries, observer) => { 
  //回调函数中打印队列
  console.log(entries)

  //option配置信息
  console.log(observer)
})

//向队列添加一个DOM对象 .container是我们之前写的渲染区标签 存放需要懒加载数据的容器
observe.observe(document.querySelector('.container'))

上图不难看出这就是一个数组,因为它有一个为0开始的下标,我在这儿称之为队列,可以从target属性看出当前标签,正是我们传入的类名为container的标签,此外还有boundingClientRect属性,这个属性里面描述了当前DOM的一些坐标信息以及高宽度,顶点等,在这里用处就不多说,最重要的还是isIntersecting属性,该属性是一个布尔值,判断是否出现在当前可视窗口内

再来看看回调函数中的第二个参数

此参数还是有点讲究的,一眼看还不能看出这是些什么东西,里面的属性基本上都为初始化状态,这是因为我们在配置里还什么都没写,在这篇文章暂时用不到此参数,关于option的详细介绍请看其他文章

既然有了这些方便的API,那我们回归正题,开始改造懒加载实现,首先要确定一个思路,我们是在滚动时,判断一个DOM是否进入了可视窗口,如果出现,那么就执行懒加载函数,如此反复,常规思路就是在容器底部加一个提示,比如正在加载中或者提示到底部了,每次出现就加载一点,数据加载完成渲染后会把提示标签重新顶回可视窗口之外,随着我们滚动网页,再次出现再次加载!现在我们需要添加一个底部提示,需要新添加一个标签如下

<body>
    <div id="app">
        <!-- 加载容器 -->
        <div class="container">
            <!-- 渲染区 -->
        </div>

        <!-- 添加的提示 -->
        <div class="footer">
            <div class="footer-loading">
                <span>到底了,哥哥~</span>
            </div>
        </div>

    </div>
    <script src="./lazyloader.js"></script>
</body>

 并添加样式

.footer {  
    height: 20vh;
    background: #ffffff;
    box-shadow: 3rem -.7rem .5rem #e7e7e7;
    display: flex
}
.footer-loading {
    margin: auto;
    color: rgb(98, 98, 98)
}

 现在是什么都没有,而且一开始这个提示标签就已经出现在了可视窗口中,这也就意味着我们的IntersectionObserver对象会立即触发一次监听,我们接着改造js

const observer = new IntersectionObserver(async (entries) => {
  //只监听一个DOM 因此我们直接取队列中的第一个元素即可
  const intersection = entries[0]
  //判断是否出现在了可视窗口中
  if (intersection.isIntersecting) {
    //此方法是我们自己实现的懒加载方法 作用于全局 本人使用的异步(意义不大) 使用同步都一样
    //callabck就是我们下面编写的lazyLoaderDom方法
    (await callback)()
  }
})

//把监听对象换成我们添加的底部提示标签
observer.observe(document.querySelector('.footer-loading'))

别急,callback函数我们会在接下来编写,该函数是一个闭包函数,在这里使用闭包是因为我们要加载的条数需要不断递增,使用闭包可以让临时变量不被回收掉从而达到递增效果且不用声明全局变量带来的污染,先准备好数据,这里准备了100+条静态数据,图片是我本地的,大家需要更换成自己的图片

pngs数据:

//方式一:
const pngs = [
  'NTIyOTgwNTE2NzE4NTYyOTQyOF8xNjgxMjk2NzgzNTk0_7.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_2.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_4.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_5.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_7.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_8.png',
  'NTIzMDY0OTU5MTkxMTM3MTA0NV8xNjgzODg3NTQxNTgx_9.png',
  ...
]

//方式二:
const pngs = new Array(150).fill('NTIyOTgwNTE2NzE4NTYyOTQyOF8xNjgxMjk2NzgzNTk0_7.png')

lazyLoaderDom方法: 

//end表示每次需要加载的条数 数量
//total表示一共有多少条数据 当加载的数量超过总数时不再进行加载
async function lazyLoaderDom(end, total) {
  //这个变量表示从第几条开始加载 默认从第一条
  let start_teep = 0
  return function start() {
    //st < start_teep + end保证每次需要加载多少条数据
    for (let st = start_teep; st < start_teep + end; st++) { 
      //在这儿判断当前加载数据量时候超过总数据量 超过即退出
      if (st + 1 > total) { 
        break
      }
      //这里我们又把数据渲染函数单独提出去写了 主要是方便维护和大家阅读 并依次传入我们pngs内的图片链接作为生成img标签的src
      resolvePngs(pngs[st])
    }
    //执行完后让当前条数改变 可以看做是当前加载进度节点
    start_teep = start_teep + end
  }
}

resolvePngs方法:

async function resolvePngs(href) {
  //获取懒加载渲染容器
  const container = document.querySelector('.container')
  //这里负责插入标签
  container.insertAdjacentHTML('beforeend', `
    <div class="role-item">
        <img src="${href}">
        <div>
            <span>AI娃娃</span>
        </div>
    </div>
  `)
}

注意,我们实现的懒加载函数是一个闭包,因此不能再 IntersectionObserver内部使用,这样会每次生成新的闭包函数,我们要把实现提取到全局中,这也是一开始IntersectionObserver实现中的(await callback)()这一步

//每次加载18条数据 刚好铺满一个屏幕 总数就是我们pngs数据的元素总个数了
const callback = lazyLoaderDom(18, pngs.length)

 完整实现代码如下:

lazyloader.js

const pngs = [
    //图片自定
]

async function lazyLoaderDom(end, total) {
  let start_teep = 0
  return function start() {
    for (let st = start_teep; st < start_teep + end; st++) { 
      if (st + 1 > total) { 
        break
      }
      resolvePngs(pngs[st])
    }
    start_teep = start_teep + end
  }
}

async function resolvePngs(href) {
  const container = document.querySelector('.container')
  container.insertAdjacentHTML('beforeend', `
    <div class="role-item">
        <img src="${href}">
        <div>
            <span>AI娃娃</span>
        </div>
    </div>
  `)
}

const callback = lazyLoaderDom(18, pngs.length)

const observer = new IntersectionObserver(async (entries) => {
  const intersection = entries[0]
  if (intersection.isIntersecting) {
    (await callback)()
  }
})

observer.observe(document.querySelector('.footer-loading'))

index.html

<!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>Document</title>
    <style>
        * { margin: 0; padding: 0 }
        body { background: rgb(253, 253, 253) }
        .container {
            padding: 1vw;
            display: flex;
            flex-wrap: wrap;
            gap: 1rem
        }
        .footer {  
            height: 20vh;
            background: #ffffff;
            box-shadow: 3rem -.7rem .5rem #e7e7e7;
            display: flex
        }
        .footer-loading {
            margin: auto;
            color: rgb(98, 98, 98)
        }
        .role-item {
            width: 15vw;
            height: 37vh;
            border: .05rem solid #ccc
        }
        .role-item > img {
            height: 30vh
        }
        .role-item > div {
            padding: 1rem
        }
        .role-item > div > span {
            font-size: 1rem;
            color: rgb(98, 98, 98)
        }
    </style>
</head>
<body>
    <div id="app">
        <!-- 加载容器 -->
        <div class="container">
            <!-- 渲染区 -->
        </div>
        <div class="footer">
            <div class="footer-loading">
                <span>到底了,哥哥~</span>
            </div>
        </div>
    </div>
    <script src="./lazyloader.js"></script>
</body>
</html>

视频效果:

20230623_170157

使用IntersectionObserver对象实现懒加载就到此结束?不!还有加载进度实现,也会给大家提供实现

四、实时加载进度

我们经常在一些技术官网,如ElementUI等官网,网页顶部都会有一个加载条,虽然实现很简单,但其实一点都不难

4-1、进度条实现 

我们又需要改动index.html啦,只需要加一点点东西即可

<body>
    <div id="app">

        <!-- 加入进度条 -->
        <div class="loading">
            <div class="loading-item"></div>
        </div>
        
        <!-- 加载容器 -->
        <div class="container">
            <!-- 渲染区 -->
        </div>
        <div class="footer">
            <div class="footer-loading">
                <span>到底了,哥哥~</span>
            </div>
        </div>
    </div>
    <script src="./lazyloader.js"></script>
</body>

结构层次包不包无所谓,这里我还是包了一层,要使用到position定位才能把进度条吸附在网页顶部,fixed、sticky都可以实现,但推荐fixed,因为鄙人使用的是fixed     (ง •_•)ง,接着加样式

.loading {
     position: fixed;
     top: 0;
     left: 0;
     width: 100vw;
     height: .3vh;
     background: rgba(255, 255, 255, 0)
}
.loading-item {
     position: relative;
     width: 0%;
     height: 100%;
     background: linear-gradient(to right, rgb(0, 251, 255), rgb(179, 0, 255));
     opacity: 0;
     transition: .5s ease
}

我使用的是渐变进度条,大家可以根据自己习惯来调颜色,我们再加.05s的动画过渡,让进度条看起来更丝滑,手动调试一波看看效果

50%

 100%

 由此看出我们在实现进度的时候,其实是调整他的width属性,即宽度,不断递增他的宽度,用一个算法与加载进度同步,那不就得到一个实时进度了吗?!效果非常之完美,但这是死的进度条,我们要让他活起来,并且是实时的!那我们思路是什么?别急,下面讲解

4-2、实时进度

一、思路

        一开始把进度条调成全透明,在触发懒加载时显示进度条,进度到达100%后在隐藏进度条,这里使用的css中的opacity调整透明0全透 1全显

二、进度监听

        把之前写的lazyLoaderDom方法拿出来遛一遛,写的这么完美不拿出来遛一遛那不是很可惜,所以我们的进度实现主要还是在于lazyLoaderDom方法内部实现,接下来开始改造它

单独提取出一个进度条方法,编写一个loadingListener方法用于监听加载进度

lazyLoaderDom方法中改造一点东西并加入函数

async function lazyLoaderDom(end, total) {
  let start_teep = 0
  return function start() {
    //注意!这里的循环变了 加入了一个新的标量stp 保证了每次进度条从0%重新开始加载
    //并且在每一张图片渲染完之后自增1
    for (let st = start_teep, stp = 0; st < start_teep + end; st++) { 
      if (st + 1 > total) { 
        break
      }
      resolvePngs(pngs[st])
      //这里是主要的实现
      stp += 1
      //接下来编写该函数实现 传入当前进度和总进度
      loadingListener(stp, end)
    }
    start_teep = start_teep + end
  }
}

loadingListener方法实现

async function loadingListener(current, end) {
  //获取到进度条标签
  const loading = document.querySelector('.loading-item')
  //一开始设为全透明 保证了淡入效果
  loading.style.opacity = 1
  //用当前数据下标数 即当前渲染数据的索引除以每次需要渲染的条数 得到当前进度
  // x100加上%字符即可得到百分比进度
  loading.style.width = `${(current / end) * 100}%`
  //加载后用一个定时器设置加载完后再次隐藏掉加载条 并且再次将进度条重置为0% 保证每次从0%开始加载 完美触发动画
  setTimeout(() => {
    loading.style.opacity = 0
    setTimeout(() => loading.style.width = `0%`, 300)
  }, 900)
}

这个地方用定时控制进度条隐藏和重置其实是不完美的,并没有考虑到加载失败,加载中断等效果,需要大家去考证,但我们在项目中遇到操作900毫秒的时候还没加载完成,就会造成bug,怎么解决就交给大家去思考,"@#.~、*!"..什么?你当然可以使用任意一个拥有进度条组件的UI框架!这里仅仅简单模拟和提供思路

完整代码:

lazyloader.js

const pngs = [
    //图片自定
]

async function lazyLoaderDom(end, total) {
  let start_teep = 0
  return function start() {
    for (let st = start_teep, stp = 0; st < start_teep + end; st++) { 
      if (st + 1 > total) { 
        break
      }
      resolvePngs(pngs[st])
      stp += 1
      loadingListener(stp, end)
    }
    start_teep = start_teep + end
  }
}

async function resolvePngs(href) {
  const container = document.querySelector('.container')
  container.insertAdjacentHTML('beforeend', `
    <div class="role-item">
        <img src="${href}">
        <div>
            <span>AI娃娃</span>
        </div>
    </div>
  `)
}

async function loadingListener(current, end) {
  const loading = document.querySelector('.loading-item')
  loading.style.opacity = 1
  loading.style.width = `${(current / end) * 100}%`
  setTimeout(() => {
    loading.style.opacity = 0
    setTimeout(() => loading.style.width = `0%`, 300)
  }, 900)
}

const callback = lazyLoaderDom(18, pngs.length)

const observer = new IntersectionObserver(async (entries) => {
  const intersection = entries[0]
  if (intersection.isIntersecting) {
    (await callback)()
  }
})

observer.observe(document.querySelector('.footer-loading'))

到这儿就告一段落了

index.html不变,把进度条标签和样式加上即可,就不再重复给出代码,最终的实现视频效果在开始已经给出

总结:熟练使用css以及js即可,其实最重要还是思路,前提你得把技术熟练,技术大佬勿喷,懒加载确实简单,市面也有现成的lazy插件,在此只是手写个实现和简单提供思路,最后,希望每个开发者的道路都是越走越开阔,不负一路的颠沛流离

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值