夜听城嚣 夜听城嚣
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏

dwfrost

前端界的小学生
首页
  • 学习笔记

    • 《JavaScript高级程序设计》
    • 前端基建与架构
  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
  • 项目实践
  • 抓包工具
  • 知识管理
  • 工程部署
  • 团队规范
bug知多少
  • 少年歌行
  • 青年随笔
  • 文海泛舟
  • 此事躬行

    • 项目各工种是如何协作的
    • TBA课程学习
收藏
  • 读书笔记

  • 专题分享

    • Git入门与开发
    • 前端面试题汇总
    • HTML和CSS知识点
    • 前端路由原理及实践
    • 性能优化之虚拟列表
    • mysql教程
    • python项目实践
    • 图片懒加载原理
      • WallectConnect开发心得
      • 企微机器人
    • 项目实践

    • 框架应用

    • 前端一览
    • 专题分享
    frost
    2023-03-17

    图片懒加载原理

    # 问题

    图片懒加载原理是一道经典的前端面试题,懒加载,顾名思义,它有点“懒”,不喊它,它就不出现。

    如果一次性加载,会怎么样:

    1.图片数量多的时候,全部加载会占用网站带宽,如果一次性发出大量的请求,会产生TCP阻塞,导致网站性能下降。

    2.图片全部渲染也会增加GPU的绘制工作量,让用户浏览器性能下降。

    3.用户的流量白白消耗。

    # 3种实现方案

    用户的滚动容器显示范围是有限的,那么只需要加载显示范围内的图片即可,这类似于列表分页。当图片所在区域出现在用户视角内时,加载该图片。如此,就可以达到懒加载的效果。

    判断元素是否出现在视口,一般有3种方案:

    • 滚动监听+scrollTop+offsetTop+clientHeight
    • 滚动监听+getBoundingClientRect()
    • intersectionObserve()

    # 滚动监听+scrollTop+offsetTop+clientHeight

    • scrollTop:指被滚动条卷去的部分。
    • offsetTop:目标元素相对父元素的位置
    • clientHeight:当前父元素容器的高度。

    三个属性之间的关系如图所示,故当scrollTop+clientHeight > offsetTop,即图片在视口内,否则图片在可视区域外。

    getBoundingClientRect

    # 滚动监听+getBoundingClientRect()

    Element.getBoundingClientRect() 方法返回一个 DOMRect (opens new window) 对象,其提供了元素的大小及其相对于视口 (opens new window)的位置。

    该对象使用 left、top、right、bottom、x、y、width 和 height 这几个以像素为单位的只读属性描述整个矩形的位置和大小。除了 width 和 height 以外的属性是相对于视图窗口的左上角来计算的。

    判断是否在视口内:当目标元素的top<父元素的top+父元素的clientHeight时,就位于父元素视口内。

    getBoundingClientRect

    # intersectionObserve()

    intersectionObserve也称为“交叉观察器”,它可以指定观察多个节点,当目标元素的可见性发生变化时,会调用观察器的回调函数。callback函数的参数是一个数组,每个成员都是一个IntersectionObserverEntry (opens new window)对象。IntersectionObserverEntry对象有个intersectionRatio属性,它表示目标元素在视口中的可见比例,完全可见时为1,完全不可见时小于等于0。

    # elment-plus源码解析

    源码链接

    找到官网的Image组件 (opens new window),用法是只需要加入lazy属性就可以开启懒加载功能。然后找到其源码,位于packages\components\image\src\image.vue。

    1.组件onMounted

    onMounted(() => {
      if (isManual.value) {
        addLazyLoadListener()
      } else {
        loadImage()
      }
    })
    const isManual = computed(() => {
      if (props.loading === 'eager') return false
      return (!supportLoading && props.loading === 'lazy') || props.lazy
    })
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11

    描述:设置了lazy属性后就开始添加懒加载监听事件(addLazyLoadListener)。

    2.监听父容器的scroll事件

    async function addLazyLoadListener() {
      if (!isClient) return
    
      await nextTick()
    
      const { scrollContainer } = props
      if (isElement(scrollContainer)) {
        _scrollContainer.value = scrollContainer
      } else if (isString(scrollContainer) && scrollContainer !== '') {
        _scrollContainer.value =
          document.querySelector<HTMLElement>(scrollContainer) ?? undefined
      } else if (container.value) {
        _scrollContainer.value = getScrollContainer(container.value)
      }
    
      if (_scrollContainer.value) {
        stopScrollListener = useEventListener(
          _scrollContainer,
          'scroll',
          lazyLoadHandler
        )
        setTimeout(() => handleLazyLoad(), 100)
      }
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24

    这里处理了3件事:

    2.1 找到可滚动的父容器

    2.2 监听父容器scroll事件

    2.3 立即触发一次handleLazyLoad

    3.判断当前图片是否在视图内。如果在,加载图片,并移除懒加载监听

    function handleLazyLoad() {
      if (isInContainer(container.value, _scrollContainer.value)) {
        loadImage()
        removeLazyLoadListener()
      }
    }
    
    1
    2
    3
    4
    5
    6
    export const isInContainer = (
      el?: Element,
      container?: Element | Window
    ): boolean => {
      if (!isClient || !el || !container) return false
    
      const elRect = el.getBoundingClientRect()
    
      let containerRect: Pick<DOMRect, 'top' | 'bottom' | 'left' | 'right'>
      if (container instanceof Element) {
        containerRect = container.getBoundingClientRect()
      } else {
        containerRect = {
          top: 0,
          right: window.innerWidth,
          bottom: window.innerHeight,
          left: 0,
        }
      }
      return (
        elRect.top < containerRect.bottom &&
        elRect.bottom > containerRect.top &&
        elRect.right > containerRect.left &&
        elRect.left < containerRect.right
      )
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26

    可以看到,element-plus是使用了getBoundingClientRect来判断元素是否出现在视口。

    # 手写3种懒加载实现

    下面依次写出3种Vue3的实现,全部代码见源码 (opens new window)。

    1. scrollTop
    <template>
      <div class="Scroll-top-wrap" @scroll="onScroll">
        <div ref="itemRef" v-for="(item, index) of imgs" :key="index" class="item">
          <img alt="" />
        </div>
      </div>
    </template>
    
    <script setup>
    import { ref } from 'vue'
    import { imgs } from './constant'
    
    const itemRef = ref(null)
    
    const onScroll = (e) => {
      const { scrollTop, clientHeight } = e.target
    
      itemRef.value.forEach((item, index) => {
        if (scrollTop + clientHeight <= item.offsetTop) return
    
        const { children } = item
        if (children?.[0] && !children[0].src) {
          children[0].src = imgs[index]
        }
      })
    }
    </script>
    
    <style lang="scss" scoped>
    .Scroll-top-wrap {
      height: 600px;
      border: 1px solid;
      overflow: scroll;
      .item {
        min-height: 200px;
        img {
          display: block;
        }
      }
    }
    </style>
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    1. getBoundingClientRect
    import { ref } from 'vue'
    import { imgs } from './constant'
    
    const parentRef = ref(null)
    const itemRef = ref(null)
    
    const onScroll = () => {
      const { top: parentTop, bottom: parentBottom } =
        parentRef.value.getBoundingClientRect()
    
      itemRef.value.forEach((item, index) => {
        const { top: childTop } = item.getBoundingClientRect()
        if (childTop < parentTop || childTop > parentBottom) return
    
        const { children } = item
        if (children?.[0] && !children[0].src) {
          children[0].src = imgs[index]
        }
      })
    }
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    1. intersectionObserve
    <template>
      <div class="Intersection-observe-wrap">
        <img
          ref="itemRef"
          v-for="(item, index) of imgs"
          :key="index"
          class="item"
          alt=""
          :data-src="item"
        />
      </div>
    </template>
    
    <script setup>
    import { onMounted, ref } from 'vue'
    import { imgs } from './constant'
    
    const itemRef = ref(null)
    
    onMounted(() => {
      const observerInstance = new IntersectionObserver((entries) => {
        entries.forEach((item) => {
          // console.dir(item.target)
    
          if (item.intersectionRatio > 0) {
            item.target.src = item.target.dataset.src
          }
        })
      })
    
      itemRef.value.forEach((item) => {
        observerInstance.observe(item)
      })
    })
    </script>
    
    <style lang="scss" scoped>
    .Intersection-observe-wrap {
      height: 600px;
      border: 1px solid;
      overflow: scroll;
      .item {
        min-height: 200px;
        display: block;
      }
    }
    </style>
    
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    #懒加载#性能优化
    上次更新: 2023/03/21, 09:28:09
    python项目实践
    WallectConnect开发心得

    ← python项目实践 WallectConnect开发心得→

    最近更新
    01
    提交代码时修改commit消息
    04-09
    02
    如何快速定位bug
    02-20
    03
    云端web项目开发踩坑
    08-25
    更多文章>
    Theme by Vdoing | Copyright © 2021-2025 dwfrost | 粤ICP备2021118995号
    • 跟随系统
    • 浅色模式
    • 深色模式
    • 阅读模式
    ×