vue-lazyLoad指令

130 阅读4分钟

lazyLoad

顾名思义,是懒加载,对于网页端来说,用户的体验非常重要,如果想要加快用户体验,更早的看到内容,不仅需要网络加速,更需要对大文件进行优化,以期达到快速响应的目的.

今天说的 lazyload 是比较常见的前端优化,主要作用于图片的加载
特点是只有当图片进入可视区的时候,才去请求加载
对于图片较多的网站,这样可以大大的加快用户访问速度,增大用户留存度


原理

首先需要一个可以发生滚动的父元素,因为需要绑定scroll事件,进行不断的检测图片是否在可视区域内

  1. 找可以滚动的元素,即overflow = scroll | auto
  2. 初始化加载
  3. 发生滚动时,判断元素是否在可视区域内
  4. 当元素已经加载过,如果继续滚动,就要停止加载
  5. 如果没有加载过,需要设置src属性
graph TD
Start --> container(寻找可以滚动的元素)
Start --> init(init-初始化加载填充页面)
container  == 发生滚动===> check{是否在可视区域内}
check ==是===>isLoad{是否加载过}
isLoad --从未加载-->a(进行加载)

代码

vue 指令初始化

    Vue.use(VueLazyload, {
      loading: 'http://localhost:3001/images/loading.gif',
      error: 'http://localhost:3001/images/error.jpg',
      preload: 1 // 距离屏幕多少开始加载
})

VueLazyload

 const VueLazyload = {
      install (Vue, options) {

        const LazyClass = Lazyload(Vue);
        const lazyload = new LazyClass(options);

        Vue.directive('lazy', {
          // el bindings vnode
          bind: lazyload.bindLazy.bind(lazyload)
        })
      }
    }

上述代码作为指令的初始化,Vue指令需要一个带有install方法的对象
主要是在Lazyload 函数,接受一个Vue 实例,返回一个lazyClass
在类lazyClass 中接受参数option = {loading:xxx,error:xxx }

这样做的目的是为了代码的简洁,保证传入函数的单纯, 还有其他写法都可以


Lazyload

主函数

function Lazyload (Vue) {
    return Class{ 
        constructor (options){
             this.options = options;
             this.lazyImgPool = [];
    }
    
     bindLazy (el, bindings) {
       Vue.nextTick(() => {
         const scrollParent = getScrollParent(el);

        if (scrollParent) {
          scrollParent.addEventListener(
            'scroll', 
            this.handleScroll.bind(this), 
            false
          );
        }

        const lazyImg = new Lazyimg({
          el,
          src: bindings.value,
          options: this.options,
          imgRender: this.imgRender.bind(this)
        });

        this.lazyImgPool.push(lazyImg);
        this.handleScroll()
      })
    }
}

其中的lazyImgPool 为保存每一个使用指令图片的数组,后期需要遍历判断
bindLazy 是指令的bind的回调函数, el是指令绑定元素,bindings 是指令中一系列绑定的对象参数
例如:<div v-example:foo.bar="baz">

binding 参数会是一个这样的对象:

{ 
    arg: 'foo',
    modifiers: { bar: true }, 
    value: /* `baz` 的值 */,
    oldValue: /* 上一次更新时 `baz` 的值 */ 
}

使用Vue.nextTick是为了确保Dom渲染完成

通过函数getScrollParent找到可以滚动的元素scrollParent
scrollParent绑定滚动事件handleScroll,这个地方一定要绑定this,否则handleScroll 中的this 会指向scrollParent,而不是当前实例,例子:

<!DOCTYPE html>
<html lang="en">
<body>
  <div id="app" style="width:100px;height:100px;background:red"></div>
<script>
    let app = document.getElementById("app")
    
    class B {
      constructor(){
        this.name = 'zs'
        app.addEventListener("click",this.fn)
      }
      fn(){
        console.log(this)
      }
    }
    let b = new B()
    b()
</script>
</body>
</html>

如果不绑定 this 的话,打印结果:

image.png

使用bind绑定this
app.addEventListener("click",this.fn.bind(this)) 打印结果: image.png

同时要把每一个使用指令的元素生成一个类Lazyimg实例,添加到lazyImgPool 中去

Lazyimg

为每一张使用指令的图片生成一个Lazyimg实例

class Lazyimg {
    constructor({ el, src, options, imgRender }) {
        this.el = el;
        this.src = src;
        this.options = options;
        this.imgRender = imgRender;
        this.loaded = false;

        this.state = {
          loading: false,
          error: false
        }
  }
  
  // 检测是否在可视范围内
  // 不用检测 bottom,加载过的图片不会再执行
   checkIsVisible () {
    const { top } = this.el.getBoundingClientRect();
    return top < window.innerHeight * (this.options.preload || 1.3);
  }
  
  // 加载图片,切换图片状态
   loadImg () {
        this.imgRender(this, 'loading');
        imgLoad(this.src).then(() => {
          this.state.loading = true;
          this.imgRender(this, 'ok');
          this.loaded = true;
        }, () => {
          this.state.error = true;
          this.imgRender(this, 'error');
          this.loaded = true;
        })
  }
}

Lazyimg实例接收参数el,src,options,imgRender

自身属性loaded 表示是否加载过
自身属性state 表示状态

有两个方法检测是否可视范围内checkIsVisable加载图片loadImg, 其中重点是loadImg

这里面有个有个技巧,loadImg 使用了传入的方法imgRender,它由外部传入 ,把自身实例传入,可以由外部接收使用,达到由外部控制的效果,自己只做一个简单的传递使用功能

其中使用到了函数loadImg,来判断图片状态

loadImg

接收一个src,返回一个promise

function imgLoad (src) {
  return new Promise((resolve, reject) => {
    const oImg = new Image();
    oImg.src = src;
    oImg.onload = resolve;
    oImg.onerror = reject;
  })
}

自己生成一个实例Image,通过实例方法 onload/onerror, 来判断传入的src是否可用

handleScroll

判断绑定的元素是否在可视范围内,并且从未加载过

handleScroll () {
      let isVisible = false;

      this.lazyImgPool.forEach(lazyImg => {
        if (!lazyImg.loaded) {
          isVisible = lazyImg.checkIsVisible();
          isVisible && lazyImg.loadImg();
        }
      })
    }

这个时候,lazyImgPool 存放的是一个个的lazyImg 实例,有属性loaded,state,方法checkIsVisible,loadImg

imgRender

交由Lazyimg使用的imgRender

const lazyImg = new Lazyimg({
    xx,
    xx,
    ...
    imgRender: this.imgRender.bind(this)
});

lazyImg中使用

loadImg(){
     this.imgRender(this, 'loading');
     imgLoad(this.src).then(() => {
      ...
      this.imgRender(this, 'ok');
      ...
    }, () => {
      ...
      this.imgRender(this, 'error');
      ...
    })
}
 imgRender (lazyImg, state) {
      const { el, options } = lazyImg;
      const { loading, error } = options;
      let src = '';

      switch (state) {
        case 'loading':
          src = loading || '';
          break;
        case 'error':
          src = error || '';
          break;
        default:
          src = lazyImg.src;
          break;
      }
      el.setAttribute('src', src);
    }

总结

此指令思路主要来源自B站小野森森
git地址

主要知识点

  1. 函数式编程控制参数个数
  2. 控制反转,imgRender 交由lazyImg统一控制 time:2022/11/1 18:52