懒加载原理及手写实现

什么是懒加载

懒加载(延迟加载) 是一种仅在需要资源时才加载它们的策略。

原理:当用户滚动页面时,判断元素是否处于可视区域,而可视区域外的元素资源(如图片)不会加载;

用处:这是一种缩短关键渲染路径长度的方法,可以缩短页面加载时间。懒加载常用来实现的功能,如:

  • 图片的懒加载
  • 列表的无限滚动
  • 计算广告元素的曝光情况
  • 可点击链接的预加载​

Lazy loading is a strategy to identify resources as non-blocking (non-critical) and load these only when needed. It's a way to shorten the length of the critical rendering path, which translates into reduced page load times. Lazy loading can occur on different moments in the application, but it typically happens on some user interactions such as scrolling and navigation.

懒加载的方案

知道了懒加载的原理的核心是判断元素是否在可视区域内,有两种方式可以实现:
(--可直接跳最后看实现--)

一是监听scroll事件时判断元素当前相对视图的位置

二是通过IntersectionObserver(重叠观察者,用于判断两个元素是否重叠,不用进行事件的监听,兼容性)。现有的方案:

三种方案都做了兼容性处理,并且都用上了 IntersectionObserver API,也就是如果浏览器支持 IntersectionObserver ,直接使用浏览器API。如果不支持,则通过滚动监听的方式,监听元素是否出现或者接近视窗区域。下面简单看一下各自的实现:

lazysizes实现

lazysizes核心源码是lazysizes-core.jslazysizes-intersection.js两个文件,其中lazysizes-core滚动监听scroll的实现,lazysizes-intersectionIntersectionObserver的实现。
首先定义了自执行函数(代码),定义了默认的参数变量,如果用户有自定义参数lazySizesConfig的话就进行合并。

(function(){
    var prop;
    var lazySizesDefaults = {
            lazyClass: 'lazyload',
            loadedClass: 'lazyloaded',
            loadingClass: 'lazyloading',
            //...省略
    };
    lazySizesCfg = window.lazySizesConfig || window.lazysizesConfig || {};
    // 合并默认配置
    for(prop in lazySizesDefaults){
            if(!(prop in lazySizesCfg)){
                    lazySizesCfg[prop] = lazySizesDefaults[prop];
            }
    }
 })();
复制代码

然后看怎么做滚动监听的,代码中定义了init函数,而实际监听的是loader._(),因此可以看loader._的实现

_: function(){
    // 获取需要lazyload的dom元素
    // lazySizesCfg.lazyClass 配置项可自定义
    lazysizes.elements = document.getElementsByClassName(lazySizesCfg.lazyClass);
    // 滚动监听scroll 
    addEventListener('scroll', throttledCheckElements, true);
}
复制代码

可以看到监听的主要实现是throttledCheckElements ,但实际上调用的是checkElements,从性能考虑对该方法做了节流处理,感兴趣看它的节流实现(代码),里面还涉及requestIdleCallback的实现。
再看checkElements如何检查元素是否在可视区域内的,以及如何把真正的资源在属性做替换。

// 触发距离
if (!defaultExpand) {
  defaultExpand = (!lazySizesCfg.expand || lazySizesCfg.expand < 1) ?
    docElem.clientHeight > 500 && docElem.clientWidth > 500 ? 500 : 370 :
  lazySizesCfg.expand;
}
// ...省略
// 遍历到当前元素时,判断是否是符合触发条件
rect = lazyloadElems[i].getBoundingClientRect();
if ((eLbottom = rect.bottom) >= elemNegativeExpand &&
    (eLtop = rect.top) <= elvH &&
    (eLright = rect.right) >= elemNegativeExpand * hFac &&
    (eLleft = rect.left) <= eLvW &&
    (eLbottom || eLright || eLleft || eLtop) &&
    (lazySizesCfg.loadHidden || isVisible(lazyloadElems[i])) &&
    ((isCompleted && isLoading < 3 && !elemExpandVal && (loadMode < 3 || lowRuns < 4)) || isNestedVisible(lazyloadElems[i], elemExpand))){
  unveilElement(lazyloadElems[i]);
  loadedSomething = true;
  if(isLoading > 9){break;}
}
复制代码

遍历到当前元素时,判断符合触发条件后会调用unveilElement中的lazyUnveil代码)来完成属性的替换。
IntersectionObserver的实现基本也是一样的处理,只是IntersectionObserver与scroll的判断可视的方式不同。

Loza.js实现

为了更好的性能,其实现直接使用Intersection Observer APIMutationObserver ,且代码只有~1k,非常的轻量。个人相关理解在代码中添加了注释:

export default function (selector = '.lozad', options = {}) {
  // 合并自定义的配置与默认配置
  const {root, rootMargin, threshold, enableAutoReload, load, loaded} = Object.assign({}, defaultConfig, options)
	// 初始化observer 、mutationObserver做元素监听 并传入监听的回调函数
  let observer
  let mutationObserver
  // 判断是否支持IntersectionObserver
  if (support('IntersectionObserver')) {
    observer = new IntersectionObserver(onIntersection(load, loaded), {
      root,
      rootMargin,
      threshold
    })
  }
	// 判断是否支持MutationObserver, enableAutoReload为属性改变时是否自动重载
  if (support('MutationObserver') && enableAutoReload) {
    mutationObserver = new MutationObserver(onMutation(load, loaded))
  }
	// 根据selector选择器,获取所有的dom节点,并遍历
  const elements = getElements(selector, root)
  for (let i = 0; i < elements.length; i++) {
    // 大图的优化,preLoad方法实现了,当大图未加载出来时,可自定义占位背景图
    preLoad(elements[i])
  }

  return {
    // 外部调用: 注册被观察者,即注册需要观察的dom节点
    observe() {
      const elements = getElements(selector, root)
			// 遍历
      for (let i = 0; i < elements.length; i++) {
        if (isLoaded(elements[i])) {
          continue
        }
				
        if (observer) {
          if (mutationObserver && enableAutoReload) {
            mutationObserver.observe(elements[i], {subtree: true, attributes: true, attributeFilter: validAttribute})
          }
					// 通过 observer.observe(target) 这一行代码即可简单的注册被观察者
          observer.observe(elements[i])
          continue
        }
        load(elements[i])
        markAsLoaded(elements[i])
        loaded(elements[i])
      }
    },
    triggerLoad(element) {
      if (isLoaded(element)) {
        return
      }
      load(element)
      markAsLoaded(element)
      loaded(element)
    },
    observer,
    mutationObserver
  }
}
复制代码

懒加载原理:可视区域的判断实现

看了上面各自方案的实现,其实都是围绕懒加载的原理,然后进行各自功能的拓展。因此我们主要要理解起核心实现,下面就一起学习一下怎么判断一个元素是否在可视区域内吧。

方式一:getBoundingClientRect

getBoundingClientRect是元素的自由的一个方法,返回值是一个 DOMRect对象,拥有left, top, right, bottom, x, y, width, 和 height属性,来说明元素在视图里面的相对位置。不熟悉的强烈建议看文档

The Element.getBoundingClientRect() method returns a DOMRect object providing information about the size of an element and its position relative to the viewport.

image.png
根据上图,我们可以得出,判断一个元素是否在视图内的条件是:top >= 0 && bottom <= viewHeight(视窗高度) 以及 left >= 0 && right <= viewWidth(视窗宽度)
因此有如下代码:

function checkElement(element):boolean {
  const viewWidth = window.innerWidth || document.documentElement.clientWidth
  const viewHeight = window.innerHeight || document.documentElement.clientHeight
  const {
    top,
    right,
    bottom,
    left,
  } = element.getBoundingClientRect() || {}
  return (
    top >= 0 &&
    bottom <= viewHeight &&
    left >= 0 &&
    right <= viewWidth
  );
}
复制代码

方式二:IntersectionObserver

性能nice!! 用法也强烈建议看文档

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。 当一个IntersectionObserver对象被创建时,其被配置为监听根中一段给定比例的可见区域。一旦IntersectionObserver被创建,则无法更改其配置,所以一个给定的观察者对象只能用来监听可见区域的特定变化值;然而,你可以在同一个观察者对象中配置监听多个目标元素。

结合上面现有库方案的实现,可以得到:


// IntersectionObserver的属性
const options = {}
const intersectionObserver = new IntersectionObserver(function(entries, observer) { 
    entries.forEach(entry => {
      	// 触发的时间
        entry.time          
      	// 根元素的位置矩形,这种情况下为视窗位置
        entry.rootBounds
      	// 被观察者的位置矩形
        entry.boundingClientRect 
      	// 重叠区域的位置矩形
        entry.intersectionRect
      	// 重叠区域占被观察者面积的比例(被观察者不是矩形时也按照矩形计算)
        entry.intersectionRatio 
      	// 目标元素
        entry.target
    });
}, options);
// start observing
const observer = intersectionObserver.observe(document.querySelector('.scrollerFooter'));
复制代码

手写图片懒加载

getBoundingClientRect

<html lang="en">

<head>
  <meta charset="UTF-8" />
  <title>Lazyload-getBoundingClientRect</title>
  <style>
    img {
      display: block;
      margin-bottom: 50px;
      height: 400px;
    }
  </style>
</head>

<body>
  <img src="" lazyload="true" data-src="assets/images/iu.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu1.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu2.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu3.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu1.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu2.jpeg" />
  <img src="" lazyload="true" data-src="assets/images/iu3.jpeg" />
  <script>
    function checkElement(element) {
      const viewWidth = window.innerWidth || document.documentElement.clientWidth
      const viewHeight = window.innerHeight || document.documentElement.clientHeight
      const {
        top,
        right,
        bottom,
        left,
      } = element.getBoundingClientRect() || {}
      return (
        top >= 0 &&
        bottom <= viewHeight &&
        left >= 0 &&
        right <= viewWidth
      )
    }
    function lazyload() {
      var eles = document.querySelectorAll('img[data-src][lazyload]')
      Array.prototype.forEach.call(eles, function (el, index) {
        var rect
        if (el.dataset.src === "")
          return
        if (checkElement(el)) {
          !function () {
            var img = new Image()
            img.src = el.dataset.src
            img.onload = function () {
              el.src = img.src
            }
            el.removeAttribute("data-src")//移除属性,下次不再遍历
            el.removeAttribute("lazyload")
          }()
        }
      })
    }
    lazyload()//刚开始还没滚动屏幕时,要先触发一次函数,初始化首页的页面图片
    document.addEventListener("scroll", lazyload)
  </script>
</body>

</html>
复制代码

IntersectionObserver

<script>
    function lazyload() {
      // IntersectionObserver的属性
      const options = { threshold: 1.0 }
      const intersectionObserver = new IntersectionObserver(function (entries, observer) {
        entries.forEach(entry => {
          console.log('entry', entry);
          const el = entry.target
          if (entry.isIntersecting) {
            !function () {
              var img = new Image()
              img.src = el.dataset.src
              img.onload = function () {
                el.src = img.src
              }
            }()
          }
        });
      }, options);
      // start observing
      var eles = document.querySelectorAll('img[data-src][lazyload]')
      Array.prototype.forEach.call(eles, function (el, index) {
        intersectionObserver.observe(el);
      })
    }
    lazyload()
  </script>
复制代码

附上代码
github.com/AutumnWhj/l…

参考

getBoundingClientRect
IntersectionObserver
懒加载和预加载

猜你喜欢

转载自juejin.im/post/7070015633995333645