IntersectionObserver - 懒加载的实现

1,182 阅读6分钟

IntersectionObserver - 懒加载的实现

前言

大家好,我是小瑜,一枚在5.21选择陪伴电脑的咸鱼靓仔,下午的时候我的24K痞幼电脑突然弹出了一条弹幕: 听说你很卷, 但是吃一顿晚饭后是否还能记得多少? 我看到后当场就不乐意了, 立马开始记录这一周收获的小小心得. 这里手动感谢下峰哥的技术分享.

什么是懒加载? 能做什么?

大家肯定背过八股文,哦~ 这个我知道,就是不会立即显示或者加载,等到使用或者看到的时候再去执行, 这样做的好处可以节省资源以及同时请求次数,以及首屏加载的时间,提升用户体验. 懒加载分为很多,路由懒加载,图片懒加载,数据懒加载.....

实现目标

首先给大家看一下完成的效果是什么样子的

123.gif

很显然,已进入页面时并没有加载图片资源,然后当滚动到图片可视区域后,才进行加载,接下来带加载来带大家实现,并进行封装优化.

图片懒加载-初步实现

通过上面的例子,实现起来的思路就是,我只要知道图片这一块区域出现在了可视区域.就加载对应的资源.

思路明确,那我如何知道进入了可视区域呢? 这边利用到了IntersectionObserver这个api,大家可以在MDN查看一下,IntersectionObserver 提供了一种异步观察目标元素与其祖先元素或顶级文档视口交叉状态的方法...叭叭叭

官方描述的一脸懵逼,这边不卖关子直接去vueUse使用现成的useIntersectionObserver,核心思路有了,接下来开始书写代码

1. 搭建架子
<template>
  <h1 class="home-page">图片懒加载</h1>
  <!-- 模拟宽度 -->
  <div class="box"></div>
  <!-- 底部 -->
  <img src="https://img1.baidu.com/it/u=2953940086,3621245794&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1683910800&t=9c742487e4501cfe3df9421d4ba09f0a" alt="">
</template>

<style lang="scss" scoped>
.box {
  width: 500px;
  height: 2000px;
  background:pink;
}
</style>

此时图片资源肯定是一开始就加载了,因为没有设置懒加载,这里下载VueUse库

npm i @vueuse/core
2. 使用api方法 - 暴力cv

查看发现当滚入到图片区域 布尔值为true , 当true的时候再执行逻辑,先打印以下if分支中的isIntersecting

import { ref } from 'vue'
import { useIntersectionObserver } from '@vueuse/core'
// 注意这里需要给需要给容器绑定ref实例
const target = ref(null)

const { stop } = useIntersectionObserver(
  target,
   // 文档还提供了observerElement这里使用不到可以先忽略
  ([{ isIntersecting }]) => {
    console.log(isIntersecting); // 打印查看isIntersecting
  },
)
3. stop方法

布尔值变化啦, 但是有一个小问题,每次滚动到可视区域都会触发布尔值的变化, 我只想要第一次变成true时就停止触发逻辑,否则懒加载就没有意义了

const { stop } = useIntersectionObserver(
  target,
  ([{ isIntersecting }], observerElement) => {
    if (isIntersecting) {
     console.log(isIntersecting)
     // 调用stop方法 这类似于wathc中的停止监听的写法
     stop()
    }
  },
)

准备工作完成后就很简单了, 在isIntersecting为true的时候给图片路径赋值就ok

   if (isIntersecting) {
     console.log(isIntersecting)
     const imgUrl = 'https://img1.baidu.com/it/u=2953940086,3621245794&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1683910800&t=9c742487e4501cfe3df9421d4ba09f0a'
     // 给img的路径赋值
     target.value.src = imgUrl
     stop()
 }
4. 解决类型报错

此时编辑器显示可能null 说明TS类型还有有null的情况 需要使用类型断言或者非空判断, 我这边就使用类型断言

const target = ref<HTMLImageElement | null>(null)
// 因为我们拿到的是img的ref 所以需要利用泛型进行定义
if (isIntersecting) {
 const url =  ref('https://img1.baidu.com/it/u=2953940086,3621245794&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=  aconsole.log('进入可视区');
 // as HTMLImageElement: 断言为HTMLImageElement类型
 const targetImg = target.value as HTMLImageElement
 targetImg.src = url.value
 stop()
}

是不是非常非常简单? 图片懒加载就这? 但是我们还是得简单优化一下,如果我有一百个组件都要用懒加载,难道要写一百遍相同的逻辑么... 此时需要封装全局指令并且注册插件

进阶 - 插件 + 懒加载指令

1. 注册插件

插件的作用: 注册全局组件 全局自定义指令... Vue2还可以注册过滤器,Vue3被砍掉了,大家使用的时候注意下

耶,我不会或者忘记了怎么办? 老规矩 看文档

// 在components下创建index.ts
export default {
  install: (app, options) => {
    // 在这里编写插件代码
  }
}
// mian.ts use
import SYUI from '@/components' SYUI是我自定义的名字(中文名字 帅瑜UI ~) 大家可以自定义名字
app.use(SYUI)

2. 注册自定义指令

这里需要使用挂载完成后的钩子 => mounted

按照文档有两个参数分别是el(dom对象) binding(传入的参数)

  • { value }=>这里我直接对bingding进行了结构,不理解的小伙伴可以打印看一下
  • { value }: { value: string } 这一步是给TS添加类型
  • app: App 这里蒙蔽的小伙伴可以去mian.ts摸一下app的类型定义就理解了,也是定义类型用的
  • 这里还添加了图片onerror事件,如果图片裂了就添加默认图片地址,可以自行添加
import { useIntersectionObserver } from '@vueuse/core'
install: (app: App<Element>) => {
    // 编写图片懒加载指令
    // el dom对象取代了 target 
    // binding 是传入的图片路径
    app.directive('lazy', {
      // 注意需要给el指定类型也就是img的类型  
      mounted(el: HTMLImageElement, { value }: { value: string }) {
        const { stop } = useIntersectionObserver(el, ([{ isIntersecting }]) => {
        console.log(isIntersecting);
          if (isIntersecting) {
            // 能进来就是true
            el.src = value
            stop()
            // 如果图片加载失败,需要给一张默认图片
            el.onerror = () => {
              el.src = '默认图片地址'
            }
          }
        })
      }
    })
  }
<template>
  <div class="home-page">home</div>
  <div class="box"></div>
  <img 
v-lazy="'https://img1.baidu.com/it/u=2953940086,3621245794&fm=253&app=138&size=w931&n=0&f=JPEG&fmt=auto?sec=1683910800&t=9c742487e4501cfe3df9421d4ba09f0a'"  src="" alt="">
</template>

完美 ~ 这样只要谁要图片懒加载 只要在标签上写 v-lazy="路径" 是不是很很棒~~

进阶 - 组件数据懒加载

在实际开发中,数据都是接口获取的,所以我想让封装的组件在进入可视区域后才会向后端发起请求,类似于图片的懒加载,是不是很好奇? 我* 还可以这样玩? 我们来实现一下

初步实现

当然这里需要vueUse提供的useIntersectionObserver方法来实现

老规矩 先搭一个架子设置高度模拟滚动到可视区域

<template>
  <div class="home-page">
    <div class="box"></div>
   // 滚动到这里时才发送请求
    <div ref="target" class="bottom">等一等再发送请求</div>
  </div>
</template>

<style lang="scss" scoped>
.box {
  width: 500px;
  height: 2000px;
  background: pink;
}

.bottom {
  width: 500px;
  height: 500px;
  background: yellow;
}
</style>

这里为了模拟请求接口 简单写个fn函数调用 实际开发只需要将其换成调用API接口函数即可

const fn = () => {
  console.log('发送请求')
}
// 这里给div绑定了ref 所以是类型是HTMLDivElement
const target = ref<HTMLDivElement | null>(null)

const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
  if (isIntersecting) {
    // 调用接口
    fn()
    // 终止执行
    stop()
  }
})

06ef6077ebbf199a3655fceb4d1d426.png

同样这也是通用的逻辑, 图片懒加载封装了插件, 那么组件懒加载封装一个hook函数(Vue3中提供同样方法的函数就是hook函数)

封装Hook函数

这里我添加了threshold(阈值可以自定义)

  • threshold:目标元素与根元素的交叉比例,可以是单一的 number 也可以是 number 数组,比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
src/composable/index.ts
/**
 * 组件数据懒加载
 * @param apiFn 传入的接口函数
 */
export function useLazyData(apiFn: () => void) {
  const target = ref(null)
  // stop 停止监听target是否进入可视区域
  const { stop } = useIntersectionObserver(target, ([{ isIntersecting }]) => {
    //isIntersecting - true 进入可视区域 false 不在可视区域
    if (isIntersecting) {
      // 每一个组件数据懒加载调用的接口是不一样的,所以把接口函数传入调用
      apiFn()
      stop()
    }
  },
    {
    // threshold(阈值可以自定义) - 0标识为完全进入可视区域时触发回调函数
      threshold: 0
  })
  // 注意 需要返回 target 让外部传入
  return {
    target
  }
}
<script setup lang="ts">
/**
 * 进入可视区域后再发送请求
 */
import { useLazyData } from '@/composable'
const fn = () => {
  console.log('发送请求')
}

const {target} = useLazyData(fn)
</script>

<template>
  <div class="home-page">
    <div class="box"></div>
    <div ref="target" class="bottom">123</div>
  </div>
</template>

最后祝大家周末愉快521快乐 别忘记平时也要和在乎的人表达心中的爱意哦~ 手动撒花★,°:.☆( ̄▽ ̄)/$:.°★