前端入门之(vue图片加载框架一)

前言: 之前做android的时候,会接触各种图片加载框架,也自己封装过,封装网络框架目的无非就是为了提高图片的复用性、减少内存消耗、监听图片的加载过程等等.换成web前端其实是一样的操作,好啦! 说了那么多我们来简单的实现一个图片加载框架,小伙伴跟紧了哦!!!

因为一直在做vue,所以我就以vue为基础来开发我们的图片加载框架了,我们新见一个vue项目,然后运行(我就以之前的vuex的demo为例子了,感兴趣的童鞋可以看看我之前写的vuex的几篇文章).

<template>
  <div class="opt-container">
    <img :src="images[0]">
  </div>
</template>

<script>
  export default {
    name: 'Lazy',
    data() {
      return {
        images: [
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=c136f7387cfe2b79161f2f93bff6cb96&imgtype=0&src=http%3A%2F%2Fpic1.cxtuku.com%2F00%2F09%2F65%2Fb3468db29cb1.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=de941561df3b6fd53b2df9bfd6c0b187&imgtype=0&src=http%3A%2F%2Fpic43.photophoto.cn%2F20170413%2F0008118236659168_b.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283185&di=aff7e8aa60813f6e36ebc6f6a961255c&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F01d60f57e8a07d0000018c1bfa2564.JPG%403000w_1l_2o_100sh.jpg',
        ]
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .opt-container {
    font-size: 0px;
  }
</style>

可以看到,很简单! 我们就是放了一个img标签.然后加载了一张图片:
这里写图片描述

现在有一个这样的需求,因为我们的图片比较大,所以当图片正在加载的时候,我们显示loading图片,然后当我们图片加载失败的时候,我们显示一个失败的默认图,当我们图片正在加载成功的时候,我们再显示.

我们来试一下哈~~

我们定义一个方法,叫loadImageAsync:

 loadImageAsync(item, resolve, reject) {
        let image = new Image();
        image.src = item.src;

        image.onload = function () {
          resolve({
            naturalHeight: image.naturalHeight,
            naturalWidth: image.naturalWidth,
            src: image.src
          });
        };

        image.onerror = function (e) {
          reject(e)
        };
      }
    }

代码很简单,我就不解释了~~ ,接下来是在我们的created的时候调用此方法:

 created() {
      let item = {src: this.images[0]};
      this.loadImageAsync(item,(response)=>{
        console.log('图片加载成功');
        console.log('图片的宽度为:'+response.naturalWidth);
        console.log('图片的高度为:'+response.naturalHeight);
      },(error)=>{
        console.log('图片加载失败');
      });
    }

我们重写运行代码看log:

[HMR] Waiting for update signal from WDS...
Lazy.vue?2392:40 图片加载成功
Lazy.vue?2392:41 图片的宽度为:600
Lazy.vue?2392:42 图片的高度为:398

我们可以看到,log里面打印出来了日志,然后把图片的宽高都打印出来了,我们试着把图片链接写错试一下:

created() {
        //我们把链接写错
      let item = {src: 11111+this.images[0]};
      this.loadImageAsync(item,(response)=>{
        console.log('图片加载成功');
        console.log('图片的宽度为:'+response.naturalWidth);
        console.log('图片的高度为:'+response.naturalHeight);
      },(error)=>{
        console.log('图片加载失败');
      });
    }

重新运行代码:

[HMR] Waiting for update signal from WDS...
Lazy.vue?2392:44 图片加载失败

可以看到,图片加载失败了~~

好啦,有了图片的监听,我们就可以操作了,我们首先准备两张图片,一张为loading(加载中图片),一张为erro(加载失败的图片).

这里写图片描述

然后我们动态的给img标签设置上src:

<template>
  <div class="opt-container">
    <img :src="currSrc">
  </div>
</template>

<script>
  const errorImg = require('./error.png');
  const loadingImg = require('./loading.png');
  export default {
    name: 'Lazy',
    data() {
      return {
        images: [
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=c136f7387cfe2b79161f2f93bff6cb96&imgtype=0&src=http%3A%2F%2Fpic1.cxtuku.com%2F00%2F09%2F65%2Fb3468db29cb1.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=de941561df3b6fd53b2df9bfd6c0b187&imgtype=0&src=http%3A%2F%2Fpic43.photophoto.cn%2F20170413%2F0008118236659168_b.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283185&di=aff7e8aa60813f6e36ebc6f6a961255c&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F01d60f57e8a07d0000018c1bfa2564.JPG%403000w_1l_2o_100sh.jpg',
        ],
        currSrc: loadingImg  //默认为加载中状态
      }
    },
    methods: {
      loadImageAsync(item, resolve, reject) {
        let image = new Image();
        image.src = item.src;

        image.onload = function () {
          resolve({
            naturalHeight: image.naturalHeight,
            naturalWidth: image.naturalWidth,
            src: image.src
          });
        };

        image.onerror = function (e) {
          reject(e)
        };
      }
    },
    created() {
      let item = {src: this.images[0]};
      this.loadImageAsync(item, (response) => {
        console.log('图片加载成功');
        console.log('图片的宽度为:' + response.naturalWidth);
        console.log('图片的高度为:' + response.naturalHeight);

        //当图片加载成功的时候,把图片的src换成目标地址
        this.currSrc = response.src;
      }, (error) => {
        console.log('图片加载失败');
        //当图片加载失败的时候,把图片的src换成失败的图片
        this.currSrc = errorImg;
      });
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .opt-container {
    font-size: 0px;
  }
</style>

代码都有注释,小伙伴应该看得懂哈~~我们运行一下:
这里写图片描述

太快了,录屏看不出loading的效果,我们把图片链接改错试试:

 let item = {src: 1111+this.images[0]};

这里写图片描述

可以看到,我们的图片加载失败就显示了一张失败的默认图,好啦! 到这里我们的简单的需求算是实现了,这时,有小伙伴就要说了,你这也太麻烦了,我只有一张图片还好,既然是框架,那就得是针对整个工程, 是的!! 我们就封装一下我们的代码,最后实现的时候,我们只需要这样写就好了:

<template>
  <div class="opt-container">
    <img v-lazy="{src:images[0]}">
  </div>
</template>

我们通过指令的形式来加载我们的图片,然后在指令中去切换图片状态,没看过指令的童鞋自己去看官网哈https://cn.vuejs.org/v2/guide/custom-directive.html

好啦,我们开动啦~~~

第一步:
我们创建一个叫lazy的文件夹,然后返回一个带install方法的对象:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description index
 */
export default {
  install(Vue, options = {}) {
    console.log('install be called!!');
  }
}

然后我们在项目的main.js用一下我们的这个插件:

// The Vue build version to load with the `import` command
// (runtime-only or standalone) has been set in webpack.base.conf with an alias.
import Vue from 'vue'
import App from './App'
import router from './router'
import store from './store'

import LazyImage from './lazy'

Vue.config.productionTip = false
Vue.use(LazyImage)
/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
  store,
  components: {App},
  template: '<App/>'
})

我们运行代码:

[HMR] Waiting for update signal from WDS...
index.js?bd6a:9 install be called!!

可以看到,我们的install方法被调用了~~

第二步:
定义lazy指令

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description index
 */
export default {
  install(Vue, options = {}) {
    Vue.directive('lazy', {
      bind: function (el, binding, vnode) {
        let s = JSON.stringify;
        let result = (
          'name: ' + s(binding.name) + '\n' +
          'value: ' + s(binding.value) + '\n' +
          'expression: ' + s(binding.expression) + '\n' +
          'argument: ' + s(binding.arg) + '\n' +
          'modifiers: ' + s(binding.modifiers) + '\n' +
          'vnode keys: ' + Object.keys(vnode).join(', ')
        )
        console.log('bind', result);
      },
      update: function () {
        console.log('update');
      },
      componentUpdated: function () {
        console.log('componentUpdated');
      },
      unbind: function () {
        console.log('unbind');
      },
    })
  }
}

我们运行代码:

[HMR] Waiting for update signal from WDS...
index.js?bd6a:20 bind name: "lazy"
value: {"src":"https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=c136f7387cfe2b79161f2f93bff6cb96&imgtype=0&src=http%3A%2F%2Fpic1.cxtuku.com%2F00%2F09%2F65%2Fb3468db29cb1.jpg"}
expression: "{src:images[0]}"
argument: undefined
modifiers: {}
vnode keys: tag, data, children, text, elm, ns, context, fnContext, fnOptions, fnScopeId, key, componentOptions, componentInstance, parent, raw, isStatic, isRootInsert, isComment, isCloned, isOnce, asyncFactory, asyncMeta, isAsyncPlaceholder
Lazy.vue?2392:50 图片加载失败

log有点多哈,不过结合我们的指令:

<img v-lazy="{src:images[0]}">

我们可以发现,我们可以从value中获取我们的src~~

好啦,定义好指令后,我们继续创建一个叫lazy的类,把一些基本的操作放在这个类中.

第三步:
实现lazy类:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description LazyDelegate
 */
const DEFAULT_ERRO_URL = require('../components/error.png');
const DEFAULT_LOADING_URL = require('../components/loading.png');
export default function (Vue) {
  return class Lazy {
    constructor({error, throttleWait, loading, attempt}) {
      this.options = {
        throttleWait: throttleWait || 200,//截流时间
        error: error || DEFAULT_ERRO_URL,//默认失败图片
        loading: loading || DEFAULT_LOADING_URL,//默认成功图片
        attempt: attempt || 3 //重试次数
      }
    }

    add(el, binding, vnode) {
      console.log('add');
    }

    update(el, binding) {
      console.log('update');
    }

    remove(el) {
      console.log('remove');
    }
  }
}

然后把我们的指令的构造函数跟我们的lazy类的方法绑定起来:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description index
 */
import lazyDelegate from './LazyDelegate';

export default {
  install(Vue, options = {}) {
    let LazyClass = lazyDelegate(Vue);
    let lazy = new LazyClass(options);
    Vue.directive('lazy', {
      bind: lazy.add.bind(lazy),
      update: lazy.update.bind(lazy),
      componentUpdated: function () {
        console.log('componentUpdated');
      },
      unbind: lazy.remove.bind(lazy),
    })
  }
}

我们找到lazy的add方法:

/**
     * 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
     * @param el 指令所绑定的元素,可以用来直接操作 DOM 。
     * @param binding
     * @param vnode
     */
    add(el, binding, vnode) {
      console.log('add');
    }

我们首先获取我们指令中的src,然后获取我们定义的error跟loading:

 _valueFormatter(value) {
      let src = value;
      let loading = this.options.loading;
      let error = this.options.error;

      // 如果value是一个object类型的时候
      if (value !== null && typeof value === 'object') {
        src = value.src;
        loading = value.loading || loading;
        error = value.error || error;
      }
      return {
        src,
        loading,
        error
      }
    }

因为我们的框架只有一个,但是我们的标签有很多个,所以我们针对每一个标签创建一个LazyListener加载工具类.然后在lazy中用一个数组统一的保存起来.
我们创建一个叫LazyListener的类:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description listener
 */
export default class LazyListener {
  constructor({el, src, error, loading, options, elRenderer}) {
    this.el = el
    this.src = src
    this.error = error
    this.loading = loading

    this.naturalHeight = 0
    this.naturalWidth = 0

    this.options = options
    //组件状态渲染方法
    this.elRenderer = elRenderer
    //初始化组件状态
    this.initState()
  }

  /**
   * 初始化组件状态
   */
  initState() {
    this.state = {
      error: false,
      loaded: false,
      rendered: false
    }
  }

}

然后在我们的lazy的add方法中创建一个listerner:

add(el, binding, vnode) {
      let {src, loading, error} = this._valueFormatter(binding.value)
      Vue.nextTick(()=>{
        const newListener = new ReactiveListener({
          el,
          loading,
          error,
          src,
          elRenderer: this._elRenderer.bind(this),
        })
      })
    }

然后把创建的listerner保存在lazy的数组中:

 return class Lazy {
    constructor({error, throttleWait, loading, attempt}) {
      //存放每一个元素的Listener加载类
      this.ListenerQueue = []
          ......
      }
/**
     * 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
     * @param el 指令所绑定的元素,可以用来直接操作 DOM 。
     * @param binding
     * @param vnode
     */
    add(el, binding, vnode) {
     ...
      Vue.nextTick(() => {
        const newListener = new ReactiveListener({
          el,
          loading,
          error,
          src,
          elRenderer: this._elRenderer.bind(this),
        })
        //将加载代理类加入到lazy的代理数组中
        this.ListenerQueue.push(newListener)
      })
    }

好啦,我们的全局lazy(经理)类创建好了,然后我们的经纪人(listener)也创建好了,我们接下来就是让经理通知经纪人干活了, 经理是一个人,经纪人有很多,所以我们把消息发到群里就可以了,我们创建一个群对话方法叫_lazyLoadHandler:

/**
     * 通知所有的listener该干活了
     * @private
     */
    _lazyLoadHandler () {
      //找出哪些是已经完成工作了的
      const freeList = []
      this.ListenerQueue.forEach((listener, index) => {
      //状态是非错误的并且是已完成的叫完成工作的人
        if (!listener.state.error && listener.state.loaded) {
          return freeList.push(listener)
        }
        //通知未完成工作的人干活了
        listener.load()
      })
      //把完成工作的listener剔除
      freeList.forEach(vm => remove(this.ListenerQueue, vm))
    }

然后我们去listener中定义一个叫load的方法:

/**
   * 加载图片的方法
   * @param onFinish 完成回调
   */
  load(onFinish) {
    console.log('load------>');
  }

因为我们的_lazyLoadHandler函数可能会被频繁的调用,这样就会阻塞js线程,体验不太好,所以我们给_lazyLoadHandler方法封装一下,加一个截流函数:

return class Lazy {
    constructor({error, throttleWait, loading, attempt}) {
      //存放每一个元素的Listener加载类
      this.ListenerQueue = []
      this.options = {
        throttleWait: throttleWait || 200,//截流时间
       ...
      }
      this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)

    }
function throttle (action, delay) {
  let timeout = null
  let lastRun = 0
  return function () {
    if (timeout) {
      return
    }
    let elapsed = Date.now() - lastRun
    let context = this
    let args = arguments
    let runCallback = function () {
      lastRun = Date.now()
      timeout = false
      action.apply(context, args)
    }
    if (elapsed >= delay) {
      runCallback()
    } else {
      timeout = setTimeout(runCallback, delay)
    }
  }
}

好啦,当我们一切准备就绪的时候,我们在我们的add方法中调用我们的lazyLoadHandler方法通知listener去加载图片:

 /**
     * 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
     * @param el 指令所绑定的元素,可以用来直接操作 DOM 。
     * @param binding
     * @param vnode
     */
    add(el, binding, vnode) {
      let {src, loading, error} = this._valueFormatter(binding.value)
      Vue.nextTick(() => {
        const newListener = new ReactiveListener({
          el,
          loading,
          error,
          src,
          // elRenderer: this._elRenderer.bind(this),
        })
        this.ListenerQueue.push(newListener)
        //通知listener去加载图片
        this.lazyLoadHandler()
        //通知listener去加载图片
        Vue.nextTick(() => this.lazyLoadHandler())
      })
    }

我们代理类的全部代码:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description LazyDelegate
 */
import LazyListener from './listener';
const DEFAULT_ERRO_URL = require('../components/error.png');
const DEFAULT_LOADING_URL = require('../components/loading.png');
export default function (Vue) {
  return class Lazy {
    constructor({error, throttleWait, loading, attempt}) {
      //存放每一个元素的Listener加载类
      this.ListenerQueue = []
      this.options = {
        throttleWait: throttleWait || 200,//截流时间
        error: error || DEFAULT_ERRO_URL,//默认失败图片
        loading: loading || DEFAULT_LOADING_URL,//默认成功图片
        attempt: attempt || 3 //重试次数
      }
      this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)

    }

    /**
     * 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
     * @param el 指令所绑定的元素,可以用来直接操作 DOM 。
     * @param binding
     * @param vnode
     */
    add(el, binding, vnode) {
      let {src, loading, error} = this._valueFormatter(binding.value)
      Vue.nextTick(() => {
        const newListener = new LazyListener({
          el,
          loading,
          error,
          src,
          // elRenderer: this._elRenderer.bind(this),
        })
        this.ListenerQueue.push(newListener)
        Vue.nextTick(() => this.lazyLoadHandler())
      })
    }

    update(el, binding) {
      console.log('update');
    }

    remove(el) {
      console.log('remove');
    }

    /**
     * 通知所有的listener该干活了
     * @private
     */
    _lazyLoadHandler () {
      //找出哪些是已经完成工作了的
      const freeList = []
      this.ListenerQueue.forEach((listener, index) => {
        if (!listener.state.error && listener.state.loaded) {
          return freeList.push(listener)
        }
        listener.load()
      })
      //把完成工作的listener剔除
      freeList.forEach(vm => remove(this.ListenerQueue, vm))
    }
    _valueFormatter(value) {
      let src = value;
      let loading = this.options.loading;
      let error = this.options.error;

      // 如果value是一个object类型的时候
      if (value !== null && typeof value === 'object') {
        src = value.src;
        loading = value.loading || loading;
        error = value.error || error;
      }
      return {
        src,
        loading,
        error
      }
    }
  }
}
function throttle (action, delay) {
  let timeout = null
  let lastRun = 0
  return function () {
    if (timeout) {
      return
    }
    let elapsed = Date.now() - lastRun
    let context = this
    let args = arguments
    let runCallback = function () {
      lastRun = Date.now()
      timeout = false
      action.apply(context, args)
    }
    if (elapsed >= delay) {
      runCallback()
    } else {
      timeout = setTimeout(runCallback, delay)
    }
  }
}

我们listener的全部代码:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description listener
 */
export default class LazyListener {
  constructor({el, src, error, loading, options, elRenderer}) {
    this.el = el
    this.src = src
    this.error = error
    this.loading = loading

    this.naturalHeight = 0
    this.naturalWidth = 0

    this.options = options
    //组件状态渲染方法
    this.elRenderer = elRenderer
    //初始化组件状态
    this.initState()
  }

  /**
   * 初始化组件状态
   */
  initState() {
    this.state = {
      error: false,
      loaded: false,
      rendered: false
    }
  }

  /**
   * 加载图片的方法
   * @param onFinish 完成回调
   */
  load(onFinish) {
    console.log('load------>');
  }
}

当我运行代码:

[HMR] Waiting for update signal from WDS...
listener.js?4bef:40 load------>

可以看到,我们的listener(经纪人)的load方法执行了~~

然后我们把我们一开始写的loadImageAsync方法搬进我们的listener中:

const loadImageAsync = (item, resolve, reject) => {
  let image = new Image()
  image.src = item.src

  image.onload = function () {
    resolve({
      naturalHeight: image.naturalHeight,
      naturalWidth: image.naturalWidth,
      src: image.src
    })
  }

  image.onerror = function (e) {
    reject(e)
  }
}

然后我们首先是渲染我们的loading:

 /**
   * 加载图片的方法
   * @param onFinish 完成回调
   */
  load(onFinish) {
    //如果重试的次数>我们设置的次数并且失败的时候我们直接不加载了
    if ((this.attempt > this.options.attempt - 1) && this.state.error) {
      onFinish && onFinish()
      return
    }
    //如果该组件已经加载完毕了直接结束
    if (this.state.loaded) {
      this.state.loaded = true
      onFinish && onFinish()
      //渲染src
      return this.render('loaded')
    }
    this.renderLoading(() => {
      this.attempt++
      loadImageAsync({
        src: this.src
      }, data => {
        this.naturalHeight = data.naturalHeight
        this.naturalWidth = data.naturalWidth
        this.state.loaded = true
        this.state.error = false
        this.render('loaded')
        onFinish && onFinish()
      }, err => {
        this.state.error = true
        this.state.loaded = false
        this.render('error')
      })
    })
  }
/**
   * 渲染loading
   * @param cb 回调
   */
  renderLoading(cb) {
    loadImageAsync({
      src: this.loading
    }, data => {
      this.render('loading')
      cb()
    }, () => {
      cb()
    })
  }

然后加载完了统一执行render方法:

/**
   * 根据状态渲染src
   * @param state
   */
  render(state) {
    this.elRenderer(this, state)
  }
 constructor({el, src, error, loading, options, elRenderer}) {
   ..
    this.elRenderer = elRenderer
    //初始化组件状态
    this.initState()
  }

其中listener的elRenderer方法其实是经理(lazy)传过去的,所以我们在lazy类中统一定义一个elRenderer方法:

 /**
     * 根据状态渲染src
     * @param listener 经纪人
     * @param state 状态
     * @private
     */
    _elRenderer(listener, state) {
      if (!listener.el) return
      const {el} = listener

      let src
      switch (state) {
        case 'loading':
          src = listener.loading
          break
        case 'error':
          src = listener.error
          break
        default:
          src = listener.src
          break
      }
      //通过js方法给el设置上src属性
      if (el.getAttribute('src') !== src) {
        el.setAttribute('src', src)
      }
      el.setAttribute('lazy', state)
    }

然后在创建listener的时候传给listener(经纪人):

add(el, binding, vnode) {
      let {src, loading, error} = this._valueFormatter(binding.value)
      Vue.nextTick(() => {
        const newListener = new LazyListener({
          el,
          loading,
          error,
          src,
          options: this.options,
          elRenderer: this._elRenderer.bind(this),
        })
        this.ListenerQueue.push(newListener)
        Vue.nextTick(() => this.lazyLoadHandler())
      })
    }

好啦,我们经理lazy的全部代码:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description LazyDelegate
 */
import LazyListener from './listener';

const DEFAULT_ERRO_URL = require('../components/error.png');
const DEFAULT_LOADING_URL = require('../components/loading.png');
export default function (Vue) {
  return class Lazy {
    constructor({error, throttleWait, loading, attempt}) {
      //存放每一个元素的Listener加载类
      this.ListenerQueue = []
      this.options = {
        throttleWait: throttleWait || 200,//截流时间
        error: error || DEFAULT_ERRO_URL,//默认失败图片
        loading: loading || DEFAULT_LOADING_URL,//默认成功图片
        attempt: attempt || 3 //重试次数
      }
      this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)

    }

    /**
     * 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
     * @param el 指令所绑定的元素,可以用来直接操作 DOM 。
     * @param binding
     * @param vnode
     */
    add(el, binding, vnode) {
      let {src, loading, error} = this._valueFormatter(binding.value)
      Vue.nextTick(() => {
        const newListener = new LazyListener({
          el,
          loading,
          error,
          src,
          options: this.options,
          elRenderer: this._elRenderer.bind(this),
        })
        this.ListenerQueue.push(newListener)
        Vue.nextTick(() => this.lazyLoadHandler())
      })
    }

    update(el, binding) {
      console.log('update');
    }

    remove(el) {
      console.log('remove');
    }

    /**
     * 通知所有的listener该干活了
     * @private
     */
    _lazyLoadHandler() {
      //找出哪些是已经完成工作了的
      const freeList = []
      this.ListenerQueue.forEach((listener, index) => {
        if (!listener.state.error && listener.state.loaded) {
          return freeList.push(listener)
        }
        listener.load()
      })
      //把完成工作的listener剔除
      freeList.forEach(vm => remove(this.ListenerQueue, vm))
    }

    /**
     * 根据状态渲染src
     * @param listener 经纪人
     * @param state 状态
     * @private
     */
    _elRenderer(listener, state) {
      if (!listener.el) return
      const {el} = listener

      let src
      switch (state) {
        case 'loading':
          src = listener.loading
          break
        case 'error':
          src = listener.error
          break
        default:
          src = listener.src
          break
      }
      //通过js方法给el设置上src属性
      if (el.getAttribute('src') !== src) {
        el.setAttribute('src', src)
      }
      el.setAttribute('lazy', state)
    }

    _valueFormatter(value) {
      let src = value;
      let loading = this.options.loading;
      let error = this.options.error;

      // 如果value是一个object类型的时候
      if (value !== null && typeof value === 'object') {
        src = value.src;
        loading = value.loading || loading;
        error = value.error || error;
      }
      return {
        src,
        loading,
        error
      }
    }
  }
}

function throttle(action, delay) {
  let timeout = null
  let lastRun = 0
  return function () {
    if (timeout) {
      return
    }
    let elapsed = Date.now() - lastRun
    let context = this
    let args = arguments
    let runCallback = function () {
      lastRun = Date.now()
      timeout = false
      action.apply(context, args)
    }
    if (elapsed >= delay) {
      runCallback()
    } else {
      timeout = setTimeout(runCallback, delay)
    }
  }
}

我们经纪人(listener)的全部代码:

/**
 * @author YASIN
 * @version [React-Native Ocj V01, 2018/8/1]
 * @date 17/2/23
 * @description listener
 */
export default class LazyListener {
  constructor({el, src, error, loading, options, elRenderer}) {
    this.el = el
    this.src = src
    this.error = error
    this.loading = loading
    this.attempt = 0 //重试次数
    this.naturalHeight = 0
    this.naturalWidth = 0

    this.options = options
    //组件状态渲染方法
    this.elRenderer = elRenderer
    //初始化组件状态
    this.initState()
  }

  /**
   * 初始化组件状态
   */
  initState() {
    this.state = {
      error: false,
      loaded: false,
      rendered: false
    }
  }

  /**
   * 加载图片的方法
   * @param onFinish 完成回调
   */
  load(onFinish) {
    //如果重试的次数>我们设置的次数并且失败的时候我们直接不加载了
    if ((this.attempt > this.options.attempt - 1) && this.state.error) {
      onFinish && onFinish()
      return
    }
    //如果该组件已经加载完毕了直接结束
    if (this.state.loaded) {
      this.state.loaded = true
      onFinish && onFinish()
      //渲染src
      return this.render('loaded')
    }
    this.renderLoading(() => {
      this.attempt++
      loadImageAsync({
        src: this.src
      }, data => {
        this.naturalHeight = data.naturalHeight
        this.naturalWidth = data.naturalWidth
        this.state.loaded = true
        this.state.error = false
        this.render('loaded')
        onFinish && onFinish()
      }, err => {
        this.state.error = true
        this.state.loaded = false
        this.render('error')
      })
    })
  }

  /**
   * 渲染loading
   * @param cb 回调
   */
  renderLoading(cb) {
    loadImageAsync({
      src: this.loading
    }, data => {
      this.render('loading')
      cb()
    }, () => {
      cb()
    })
  }

  /**
   * 根据状态渲染src
   * @param state
   */
  render(state) {
    this.elRenderer(this, state)
  }
}
const loadImageAsync = (item, resolve, reject) => {
  let image = new Image()
  image.src = item.src

  image.onload = function () {
    resolve({
      naturalHeight: image.naturalHeight,
      naturalWidth: image.naturalWidth,
      src: image.src
    })
  }

  image.onerror = function (e) {
    reject(e)
  }
}

然后我们的测试类中的全部代码:

<template>
  <div class="opt-container">
    <img v-lazy="{src:images[1]}">
  </div>
</template>

<script>
  export default {
    name: 'Lazy',
    data() {
      return {
        images: [
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=c136f7387cfe2b79161f2f93bff6cb96&imgtype=0&src=http%3A%2F%2Fpic1.cxtuku.com%2F00%2F09%2F65%2Fb3468db29cb1.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283186&di=de941561df3b6fd53b2df9bfd6c0b187&imgtype=0&src=http%3A%2F%2Fpic43.photophoto.cn%2F20170413%2F0008118236659168_b.jpg',
          'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1533137283185&di=aff7e8aa60813f6e36ebc6f6a961255c&imgtype=0&src=http%3A%2F%2Fimg.zcool.cn%2Fcommunity%2F01d60f57e8a07d0000018c1bfa2564.JPG%403000w_1l_2o_100sh.jpg',
        ]
      }
    }
  }
</script>

<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped>
  .opt-container {
    font-size: 0px;
  }
</style>

运行代码:
这里写图片描述

我们写一个错误的图片链接试试:

<template>
  <div class="opt-container">
    <img v-lazy="{src:111+images[1]}">
  </div>
</template>

这里写图片描述

好啦~~ 已经基本实现了我们的效果了~~篇幅有点长了,写得我都睡着了,细心的小伙伴可能会发现,代码长得好像一个叫vue-lazyload的框架,是的!! 我就是一点一点在解析它的源码,哈哈哈!!! 小伙伴不要失望哈,学习别人的东西不一定就是很丢丑的一件事情,别人牛逼干嘛不去学习呢??

好啦~ 先附上vue-lazyload框架的地址:
https://github.com/hilongjw/vue-lazyload

当然!! 我这个只是一个demo,小伙伴千万不要直接丢到项目中哦,要用的话直接去拖vue-lazyload的代码就好了.

这一节先结束了,下一节我将带大家一起实现(懒加载、缓存、监听等)未实现的功能,睡觉哒!!!!!! 欢迎入群,欢迎交流~~~~

这里写图片描述

猜你喜欢

转载自blog.csdn.net/vv_bug/article/details/81347644