Vue-Lazyload 图片懒加载学习

4,636 阅读5分钟

一、说明与使用

Vue-Lazyload作为Vue框架的图片懒加载的插件,通过指令的方式在Vue开发中使用。

如下来自官方使用说明:

1 引入

import VueLazyload from 'vue-lazyload'  // 引入

Vue.use(VueLazyload)  // 不带参数注册指令

// or with options    // 带参数注册指令
Vue.use(VueLazyload, {  
  preLoad: 1.3,  // proportion of pre-loading height, 个人理解是图片加载前目标元素位置范围
  error: 'dist/error.png',  // 加载错误时
  loading: 'dist/loading.gif', // 加载中
  attempt: 1  // 下载图片时错误重连次数
  //... 还可以设置更多参数,如触发事件
})

2 使用

<!-- 单个图片实现懒加载 ->
<ul>
  <li v-for="img in list">
    <img v-lazy="img.src" >
  </li>
</ul>

<!-- 通过定义懒加载的图片容器,类似于事件委托  ->
<div v-lazy-container="{ selector: 'img' }">
  <img data-src="//domain.com/img1.jpg">
  <img data-src="//domain.com/img2.jpg">
  <img data-src="//domain.com/img3.jpg">
</div>


二、原理探索

1. 核心思想

  • 先将图片的src设置为同一张图片或者不设置,同时给img标签设置一个特殊属性,例如:data-src用于存放图片的真实预览地址;
  • 若图片未进入可视区域时,展示同一张图片或者直接不展示图片,此时就不会发生http请求,当图片进入可视区域时,将data-src上的值赋给src,此时再发送http请求。

2  Vue-Lazyloader 懒加载的原理


  • 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件);
  • 上面的dom事件回调中,会遍历listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源;
  • 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容


3. 核心代码分析

一下代码版本:1.3.3

constructor ({ preLoad, error, throttleWait, preLoadTop, dispatchEvent, loading, attempt, silent = true, scale, listenEvents, hasbind, filter, adapter, observer, observerOptions }) {     // .... 其它属性      this.ListenerQueue = []  // 监控事件变化队列      this.TargetQueue = []   // 触发事件的目标元素的队列
      this.options = {
        throttleWait: throttleWait || 200, // 防抖时间
        preLoad: preLoad || 1.3,  // 可视范围倍数
        preLoadTop: preLoadTop || 0,
        error: error || DEFAULT_URL, // 错误图片地址
        loading: loading || DEFAULT_URL, // 加载样式
        attempt: attempt || 3,
        ListenEvents: listenEvents || DEFAULT_EVENTS,
        // .... 其它代码
      }
      this._initEvent()
      this._imageCache = new ImageCache({ max: 200 })
       
      // 懒加载函数
      this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)
      this.setMode(this.options.observer ? modeType.observer : modeType.event)
    }

在Lazy构造函数中,初始化监控事件变化的队列,目标DOM队列,以及相关Option。

构造函数中,首先进行了事件注册,设置目标元素,定义懒加载函数lazyLoadHandler。

事件注册:
this.Event.listeners[event].push(func) // _initEvent
this.options.ListenEvents.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler))  // _initListener_initListen

其中:

DEFAULT_EVENTS 默认的触发的事件类型范围有:

'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'

DEFAULT_URL 加载错误后的默认图片为,最小base64 的透明图片

'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'


Lazy load模块和listener模块他们的业务职责分工明确。

lazy负责和dom相关的处理,包括为dom创建listener,为target注册dom事件,渲染dom;listener只负责状态的控制,在不同状态执行不同的业务。


lazyLoadHandler方法

    /** 发现那些图片节点在可是范围内,则触发load加载     * find nodes which in viewport and trigger load     * @return           */    _lazyLoadHandler () {      const freeList = []      this.ListenerQueue.forEach((listener, index) => {        if (!listener.el || !listener.el.parentNode) {          freeList.push(listener)        }        const catIn = listener.checkInView()        if (!catIn) return        listener.load()      })      freeList.forEach(item => {        remove(this.ListenerQueue, item)        item.$destroy()      })    }

Load 方法

  /*   * try load image and  render it   * @return   */  load (onFinish = noop) {    if ((this.attempt > this.options.attempt - 1) && this.state.error) { // 尝试次数过限      if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`)      onFinish()      return    }    if (this.state.rendered && this.state.loaded) return // 如果状态已经处于render或者加载完成状态    if (this._imageCache.has(this.src)) { // 如果加载完成      this.state.loaded = true      this.render('loaded', true)      this.state.rendered = true      return onFinish()    }    this.renderLoading(() => { // 加载中      this.attempt++ // 尝试次数加1      this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options)      this.record('loadStart')      loadImageAsync({        src: this.src      }, data => { // 图片加载完成        this.naturalHeight = data.naturalHeight        this.naturalWidth = data.naturalWidth        this.state.loaded = true         this.state.error = false        this.record('loadEnd')        this.render('loaded', false)        this.state.rendered = true        this._imageCache.add(this.src)        onFinish()      }, err => { // 图片加载错误        !this.options.silent && console.error(err)        this.state.error = true        this.state.loaded = false        this.render('error', false)      })    })  }

loadImageAsync 图片异步加载方法,

const loadImageAsync = (item, resolve, reject) => {  let image = new Image() // 初始化图片对象  if (!item || !item.src) { // 图片是否有地址    const err = new Error('image src is required')    return reject(err)  }  image.src = item.src  image.onload = function () {    resolve({      naturalHeight: image.naturalHeight,      naturalWidth: image.naturalWidth,      src: image.src    })  }  image.onerror = function (e) {    reject(e)  }}

设置图片Render

    /**    * set element attribute with image'url and state    * @param  {object} lazyload listener object    * @param  {string} state will be rendered    * @param  {bool} inCache  is rendered from cache    * @return    */    _elRenderer (listener, state, cache) {      if (!listener.el) return      const { el, bindType } = listener      // 根据不同状态加载不同的图片资源      let src      switch (state) {        case 'loading':          src = listener.loading          break        case 'error':          src = listener.error          break        default:          src = listener.src          break      }      if (bindType) {// v-lazy: 后面的内容, 代表绑定的是这个属性        el.style[bindType] = 'url("' + src + '")'// 用于lazy load 背景图片      } else if (el.getAttribute('src') !== src) {// 普通lazyload image        el.setAttribute('src', src)      }      el.setAttribute('lazy', state)// 自定义属性 lazy,用于给用于 根据此进行class搜索,设置指定状态的样式      this.$emit(state, listener, cache) // 触发当前状态的回调函数      this.options.adapter[state] && this.options.adapter[state](listener, this.options)// 触发adapter中的回调函数      if (this.options.dispatchEvent) {        const event = new CustomEvent(state, {          detail: listener        })        el.dispatchEvent(event)      }    }

为新增节点

主要操作:找到对应的target(用于注册dom事件的dom节点;比如:页面滚动的dom节点),为其注册dom事件;为当前dom创建Listenr并添加到listener queue中。最后代用lazyLoadHandler()函数,加载图片

    /*     * add image listener to queue     * @param  {DOM} el     * @param  {object} binding vue directive binding     * @param  {vnode} vnode vue directive vnode     * @return     */    add (el, binding, vnode) {      if (some(this.ListenerQueue, item => item.el === el)) {        this.update(el, binding)        return Vue.nextTick(this.lazyLoadHandler)      }      let { src, loading, error } = this._valueFormatter(binding.value)      Vue.nextTick(() => {        src = getBestSelectionFromSrcset(el, this.options.scale) || src        this._observer && this._observer.observe(el)        const container = Object.keys(binding.modifiers)[0]        let $parent        // 如果使用了container 修饰符, 那么查找我们定义的contianer; 如果没有使用当前dom所在最近的滚动parent
        // 这个contianer是用于 设置监听dom事件的dom对象, 他的事件触发回调会触发图片的加载操作        if (container) {          $parent = vnode.context.$refs[container]          // if there is container passed in, try ref first, then fallback to getElementById to support the original usage          $parent = $parent ? $parent.$el || $parent : document.getElementById(container)        }        if (!$parent) {          $parent = scrollParent(el)        }
        // / 在当前dom绑定到vdom中, 为当前dom创建一个监听事件(此事件用于触发当前dom在不同时期的不同处理操作), 并将事件添加到事件队列里面
        const newListener = new ReactiveListener({          bindType: binding.arg,          $parent,          el,          loading,          error,          src,          elRenderer: this._elRenderer.bind(this),          options: this.options,          imageCache: this._imageCache        })        this.ListenerQueue.push(newListener)        if (inBrowser) {          this._addListenerTarget(window) // 增加目标对象,window          this._addListenerTarget($parent)// 增加目标对象, 父级元素        }        this.lazyLoadHandler() // 立即调用懒加载        Vue.nextTick(() => this.lazyLoadHandler()) // dom渲染后立即调用懒加载      })    }


检查元素是否在可是范围内

首先看y轴方向的判断:this.rect.top < window.innerHeight * this.options.preLoad, 是dom的顶部是否到了preload的位置;this.rect.bottom > this.options.preLoadTop 判断dom的底部是否到达了preload的位置 关于x轴方向就不做解析了,实现同y轴。 

  /*   *  check el is in view   * @return {Boolean} el is in view   */  checkInView () {    this.getRect()    return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) &&            (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0)  }

三、简易版的懒加载

html

其中,img标签就是主角,data-src属性上面保存着我们后面需要动态加载的图片地址,初始化时图片没有设置任何链接

<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>
<img class="imgLazyLoad" data-src="http://office.qq.com/images/title.jpg" />
<ul>
    <li>1</li>
    <li>2</li>
    <li>3</li>
    <li>4</li>
    <li>5</li>
</ul>

js

(function(){//立即执行函数
    let imgList = [],delay,time = 250,offset = 0;
    function _delay(){//函数防抖
        clearTimeout(delay);
        delay = setTimeout(() => {
            _loadImg();
        },time)
    };
    function _loadImg(){//执行图片加载
        for(let i = 0 ; i < imgList.length; i++){
            if(_isShow(imgList[i])){
                imgList[i].src = imgList[i].getAttribute('data-src');
                imgList.splice(i,1);
            }
        }
    };
    function _isShow(el){//判断img是否出现在可视窗口
        let coords = el.getBoundingClientRect();
        return (coords.left >= 0 && coords.left >= 0 && coords.top) <= (document.documentElement.clientHeight || window.innerHeight) + parseInt(offset);
    };
    function imgLoad(selector){//获取所有需要实现懒加载图片对象引用并设置window监听事件scroll
        _selector = selector || '.imgLazyLoad';
        let nodes = document.querySelectorAll(selector);
        imgList = Array.apply(null,nodes);
        window.addEventListener('scroll',_delay,false) 
    };
    imgLoad('.imgLazyLoad')
})()

  • imgList:保存所有图片节点的数组
  • delay:保存的是setTimeout生成的引用
  • time:控制防抖函数延迟执行的时间
  • offset:设置图片距离可视区域多远则立即加载的偏差值


参考资料:

blog.csdn.net/qq_18851347…

vue-lazyload github文档