jquery 加载显示loading图标_Vue实战:图片懒加载组件

注意 : 文中 "加载区域" = 可视区域(父容器高度) * preload(用户使用时配置项中传入) = 可视区域(父容器高度) + 预加载区域

889ceaa0be7340e8dc486565a77decfc.png

当访问页面时,如果一次性请求当前页面中的所有图片,会占用很大的资源。而图片懒加载所实现的功能,就是只加载用户加载区域的图片,而加载区域外的图片并不会进行资源请求,当页面滚动时会对当前加载区域的内容继续进行加载。

HTML加载过程如下:

b0fcd916d9eafb92eccd24a9b06733e0.png

已加载的图片为用户已经浏览过的内容,处于loading的图片是用户当前正在浏览的内容,之后会替换为图片的真实路径,而还未加载的图片处于可视区域下方,没有src属性,只有在用户浏览时才会进行加载。

在了解了图片懒加载的大致工作流程后,我们开始使用Vue自己实现一个图片懒加载组件。

  • 源码地址
  • demo 展示

组件使用方式分析

这里我们设计一下用户将如何使用我们实现的组件:

import Vue from 'vue';
import LazyLoad from '@/components/lazy-load';

Vue.use(LazyLoad, {
  preload: 1.3, // 加载区域相对于可视区域的比例,即加载区域 = 容器高度(可视区域) * preload
  error: require('@/assets/imgs/error.png'), // 图片加载失败时显示
  loading: require('@/assets/imgs/loading.png') // 图片加载过程中显示
});

上述代码中,我们使用Vue.use来使用LazyLoad组件,说明该组件是一个Vue插件。在页面中的使用方式如下:

<template>
  <div class="container">
    <img class="img" v-for="(img,index) in images" v-lazy="img" :key="index" alt="">
  </div>
</template>

可以看到在img标签上,我们用到了v-lazy指令。这是Vue中的自定义指令,方便我们进行dom操作,以及封装一些可复用的逻辑。

组件安装

在之前我们提到过要使用Vue.use来使用组件,所以组件要暴露install方法。

// index.js
import Lazy from '@/components/lazy-load/lazy';

const install = (Vue, options) => {
  const lazy = new Lazy(Vue,options);
  Vue.directive('lazy', {
    bind: lazy.add.bind(lazy),
    unbind: lazy.destroy.bind(lazy)
  });
};

export default install;

上边代码中Lazy是一个class,用来书写组件的逻辑,代码如下:

class Lazy {
  constructor (Vue, options) {
    this.Vue = Vue;
    this.options = options;
  }
  // 为每个img元素绑定bind钩子函数
  add (el, binding) {
    this.Vue.nextTick(()=> {

    })
  }
  // 销毁时绑定unbind函数
  destroy () {

  }
}

export default Lazy;

需要注意由于我们绑定的是自定义指令的bind钩子函数,在钩子函数执行的时候会获取不到绑定指令的元素。通过Vue.nextTick方法,可以确保逻辑在DOM渲染完毕后执行,准确获取到页面中的元素

首次加载图片

要想渲染可视区域中对应的图片,逻辑如下:

  • 获取el(绑定自定义指令的元素)的父容器元素
  • 将所有el收集起来
  • 判断收集的el是否在加载区域内,以及是否被加载过
  • 加载"加载区域"内没有被加载过的图片

代码如下:

// lazy.js
import ReactiveListener from '@/components/lazy-load/listener';

class Lazy {
  constructor (Vue, options) {
    this.Vue = Vue;
    this.options = options;
    this.listenerQueue = [];
    this.parent = undefined;
    // 将原型上的方法绑定到自身的属性上
    this.lazyHandler = this.lazyHandler.bind(this);
  }

  add (el, binding) {
    // 确保能获取到dom元素
    this.Vue.nextTick(() => {
      // 获取父容器元素
      this.parent = this.getScrollParent(el);
      // ReactiveListener 包含绑定指令的每一项元素的信息
      const listener = new ReactiveListener({
        el,
        src: binding.value,
        parent: this.parent,
        lazyOptions: this.options
      });
      // 将收集队列
      this.listenerQueue.push(listener);
      // 加载队列中收集的元素
      this.lazyHandler();
    });
  }

  lazyHandler () {
    this.listenerQueue.forEach(listener => {
      // 加载区域内并且没有被加载过的文件需要加载
      if (listener.checkInView() && (listener.state === 'init')) {
        listener.load();
      }
    });
  }

  getScrollParent (el) {
    let parent = el.parentNode;
    while (parent && parent !== window) {
      // 返回一个对象,该对象包含在应用激活的样式表和解析这些值可能包含的任何基础计算后的一个元素的所有CSS属性值。
      // 单独的CSS属性值通过对象提供的APIs或者通过CSS属性名索引访问
      const { overflow, overflowY } = getComputedStyle(parent);
      if (/scroll|auto/.test(overflow) || /scroll|auto/.test(overflowY)) {break;}
      parent = parent.parentNode;
    }
    return parent;
  }
}

export default Lazy;

Lazy类中,我们会通过递归调用el.parentNode,来不停的查找其父元素,直到找到设置了overflow属性的元素。该元素就是我们要找的容器元素,要加载的图片会在容器元素内滚动。

lazyHandler中我们检查所有收集的listener是否在加载区域内以及是否加载过,对于加载区域内没有加载过的元素调用load方法。

listenerReactiveListener的实例,用来描述每一个被加载图片的信息,其内部实现如下:

class ReactiveListener {
  constructor (options) {
    const { lazyOptions } = options;
    this.el = options.el;
    this.src = options.src;
    this.parent = options.parent;
    this.preload = lazyOptions.preload;
    this.loading = lazyOptions.loading;
    this.error = lazyOptions.error;
    this.state = 'init'; // init, pending, success, failure
  }

  // 检查元素是否在加载区域内
  checkInView () {
    const { top, height } = this.parent.getBoundingClientRect();
    const { top: elTop } = this.el.getBoundingClientRect();
    return elTop - height * this.preload < top;
  }
  // 加载图片
  load () {
    this.state = 'pending';
    this.el.src = this.loading;
    this.loadImage(() => {
      this.state = 'success'
      this.el.src = this.src
    },() => {
      this.state = 'failure'
      this.el.src = this.error
    })
  }
  // 模拟图片异步加载过程
  loadImage (resolve, reject) {
    const image = new Image();
    image.src = this.src;
    image.addEventListener('load', resolve);
    image.addEventListener('error', reject);
  }
}

export default ReactiveListener;

checkInView方法内部判断了图片是否在加载区域内,其计算逻辑如下图:

130779ff704cdedaac0056e4211cf311.png

当 图片距离视口的top - 父容器 * 预加载比例 < 父容器距离视口的top 时,说明图片在加载区域内部,需要加载。加载区域会低于父容器底部的一定位置,这样会在用户的可视区域外再提供一些预加载区域,用于多加载一些图片,从而提升用户体验。

在图片加载时,我们通过创建一个Image实例。为image设置src属性后,通过监听load以及error事件来模拟其加载过程,便于真实图片在加载中显示loading状态图片以及加载失败显示error状态图片。

容器滚动时加载

当用户滚动父容器时,可视区域发生了变化,此时我们需要对所有收集的listener中处于未加载状态的图片进行加载:

import ReactiveListener from '@/components/lazy-load/listener';

class Lazy {
  constructor (Vue, options) {
    // omit some code...
    // 将原型上的方法绑定到自身的属性上
    this.lazyHandler = this.lazyHandler.bind(this);
  }

  add (el, binding) {
    // 确保能获取到dom元素
    this.Vue.nextTick(() => {
      this.parent = this.getScrollParent(el);
      if (!this.hasBindScroll) {
        this.parent.addEventListener('scroll', this.lazyHandler);
        this.hasBindScroll = true;
      }
      // omit some code ...
    });
  }
  lazyHandler () {
    this.listenerQueue.forEach(listener => {
      if (listener.checkInView() && (listener.state === 'init')) {
        listener.load();
      }
    });
  }
}

hasBindScroll用来防止对parent多次绑定scroll,在首次绑定之后就会设置为true

在父容器滚动的时候执行lazyHandler方法,用于加载"加载区域"内的图片。需要注意的是:要提前绑定lazyHandlerthis指向,否则this将会指向parent

每次滚动的时候,都会执行lazyHandler中的逻辑,当图片内容较多时,性能会比较差,这里我们可以使用节流函数来进行优化。即用户滚动期间,我们可以设置间隔时间,在特定间隔时间内,只会执行一次lazyHandler函数,极大的减少了函数执行次数:

import ReactiveListener from '@/components/lazy-load/listener';

class Lazy {
  constructor (Vue, options) {
    // omit some code ...
    // 指定this指向并且每200ms执行一次
    this.lazyHandler = this.throttle(this.lazyHandler.bind(this), 200);
  }
  // omit some code ...

  lazyHandler () {
    this.listenerQueue.forEach(listener => {
      if (listener.checkInView() && (listener.state === 'init')) {
        listener.load();
      }
    });
  }

  throttle (fn, wait = 0) {
    let timerId = null;
    return function (...args) {
      if (timerId) {return;}
      timerId = setTimeout(() => {
        fn(...args);
        timerId = null;
      }, wait);
    };
  }
}

export default Lazy;

到这里,我们就可以通过chrome浏览器控制台看到笔者在文章开始时截图的效果了。

结语

在文末对实现组件所需要的知识点以及其文档链接进行整理,方便进行查阅和回顾

  • Vue.use: 安装一个Vue.js插件。如果插件是一个对象,它必须暴露一个install方法。如果它是一个函数,它将会作为安装方法来对待。
  • Vue.nextTick: 在下一次DOM更新循环之后执行延迟回调。在你已经更改一些数据之后,立即使用它来获取DOM更新后的数据。
  • href="https://cn.vuejs.org/v2/guide/custom-directive.html#ad">Vue自定义指令: 复用在普通DOM上的一些底层访问。
  • Element.getBoundingClientRect: 返回一个元素的大小以及相对于视口的位置
  • getComputedStyle: 返回包含一个元素所有CSS属性值的一个对象,对象中的属性值是在应用激活样式表以及解析这些值可能包含的基础计算之后的值

参考资料:

  • vue-lazyload
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值