[图片懒加载][Angular]使用Intersection Observer实现图片懒加载并在Angular中使用

2,106 阅读4分钟

链接: https://blog.angularindepth.com/a-modern-solution-to-lazy-loading-using-intersection-observer-9280c149bbc

现如今Web应用的性能如今越来越重要,有一个影响页面加载的很重要因素就是图片,尤其是页面中有很多图片的时候。如果可能的话,对这些图片使用懒加载是坠吼的,也就是只有当用户滚动到图片位置时才去加载这张图片,这样做的确提升了页面的首次加载速度。对于移动端而言,这样也可以节约用户的流量。

当前懒加载图片的方法

要想知道当前是否需要加载一张图片,我们需要坚持当前页面可见范围内这张图片是否可见。如果是,则加载。检查方法:我们可以通过事件和事件处理器来监测页面滚动位置、offset值、元素高度、视窗高度并计算出这张图片是否在可见视窗内。

但是,这样做也有几点副作用:

  • 由于所有的计算将在JS的主线程进行,因此可能会带来性能问题;
  • 每次执行滚动时,以上计算都会执行一遍,如果我们的图片在最底部的,无形间浪费了很多资源;
  • 如果页面中有很多图片,这些计算将会十分占用资源。

一个更加现代化的解决方案

最近我阅读了一个比较新的DOM API,Interction Observer API。这个API提供了一种侦测元素与当前视窗相交的方法,同时当这个元素与视窗开始相交或者相离时可以触发执行一个回调函数。因此,我们就不需要在JS主线程中进行其他多余的计算。

除了侦测元素与视窗是否相交之外,Intersection Observer API还可以侦测元素与视窗相交的百分比。只需要在创建一个intersection observer时的options中设置threshold参数。threshold参数接受一个0到1。当threshold值为0时意味着一旦元素的第一个像素出现在视窗中时,回调函数就会被触发,值为1时则是元素完全显示时才会触发回调函数。

threshold也可以是一个由0到1之间的数组成的数组,这样每当图片与视窗相交范围达到这个值时,回调函数就会被触发。codepen这里有一个案例,解释了threshold数组是如何工作的。

总的来说,通过Intersection Observer API实现的懒加载主要包括以下几个步骤:

  • 创建一个intersection observer实例;
  • 通过这个实例可以观测到我们希望懒加载的元素的可见情况;
  • 当元素出现在视窗中,加载元素;
  • 一旦元素加载完成,则停止对他的观测;

在Angular中,我们可以将这些功能放进一个指令里。

将以上功能封装成一个Angular指令

由于我们这里需要改变DOM元素,因此我们可以封装一个Angular指令作用于我们想懒加载的元素上。

这个指令会有一个输出事件,这个事件会在元素出现在视窗后触发,在我们的场景下,这个事件是显示这个元素;

import { Directive, EventEmitter, Output, ElementRef } from '@angular/core';

@Directive({
  selector: '[appDeferLoad]'
})
export class DeferLoadDirective {
  @Output() deferLoad: EventEmitter<any> = new EventEmitter();

  private _intersectionObserver?: IntersectionObserver;

  constructor(
    private _elemRef: ElementRef,
  ) {}
}

创建一个intersection observer并开始观察元素

组件视图初始化成功后,我们需要创建一个intersection observer实例,创建过程接受两个参数:

  • 一个元素与视窗相交百分比达标后触发的回调函数
  • 一个可选的对象options
ngAfterViewInit() {
    this._intersectionObserver = new IntersectionObserver(entries => {
      this._chechForIntersection(entries);
    }, {});
    this._intersectionObserver.observe(<Element>this._elemRef.nativeElement);
  }

private _chechForIntersection(entries: IntersectionObserverEntry[]) {}

侦测,加载,取消观察

回调函数_chechForIntersection()应该在侦测到元素与视窗相交后执行,包括向外emit一个方法deferLoad,取消观察元素,断开这个intersection observer。

private _chechForIntersection(entries: IntersectionObserverEntry[]) {
    entries.forEach((entry: IntersectionObserverEntry) => {
        if (this._checkIfIntersecting(entry)) {
            this.deferLoad.emit();

            // 取消观察元素,断开这个intersection observer
            this._intersectionObserver.unobserve(this._elemRef.nativeElement);
            this._intersectionObserver.disconnect();
        }
    });
}

private _checkIfIntersecting(entry: IntersectionObserverEntry) {
    return entry.isIntersecting && entry.target === this._elemRef.nativeElement;
}

使用

将directive在模块中导入,并在declarations中声明;

<div
    appDeferLoad
    (deferLoad)="showMyElement=true">
    <my-element
       *ngIf=showMyElement>
      ...
    </my-element>
</div>

这样就会给这个div加上延迟加载,并在显示后触发(deferLoad)中的方法。通过这个方法我们可以控制元素的显示隐藏

总结

完整的指令如下所示

// defer-load.directive.ts
import { Directive, Output, EventEmitter, ElementRef, AfterViewInit } from '@angular/core';

@Directive({
  selector: '[appDeferLoad]'
})
export class DeferLoadDirective implements AfterViewInit {

  @Output() deferLoad: EventEmitter<any> = new EventEmitter();

  private _intersectionObserver: IntersectionObserver;

  constructor(
    private _elemRef: ElementRef
  ) { }

  ngAfterViewInit() {
    this._intersectionObserver = new IntersectionObserver(entries => {
      this._checkForIntersection(entries);
    }, {});
    this._intersectionObserver.observe(<Element>this._elemRef.nativeElement);
  }

  private _checkForIntersection(entries: IntersectionObserverEntry[]) {
    console.log(entries);
    entries.forEach((entry: IntersectionObserverEntry) => {
      if (this._checkIfIntersecting(entry)) {
        this.deferLoad.emit();

        // 取消观察元素,断开这个intersection observer
        this._intersectionObserver.unobserve(this._elemRef.nativeElement);
        this._intersectionObserver.disconnect();
      }
    });
  }

  private _checkIfIntersecting(entry: IntersectionObserverEntry) {
    return (<any>entry).isIntersecting && entry.target === this._elemRef.nativeElement;
  }

}

最后了最后了

这个API还处于WD(working draft)阶段,对于不支持的浏览器例如IE全系列,EDGE15以下版本,我们仍需要使用文章开头提到的方案。当然,本文只是实现了一个Intersection onserver在Angular应用中的使用,同样你也可以在React,Vue等其他框架中使用,原理都是一样的。

结束!哈!