img/img.vue

<template>
    <div class="ion-img img" :class="{'img-loaded':isLoaded,'img-unloaded':!isLoaded}" :src="src"
         :style="{'width':w,'height':h}">
        <transition name="fade">
            <img v-show="isLoaded" ref="img" :alt="alt" :src="srcValue" :width="w" :height="h">
        </transition>
    </div>
</template>
<style lang="scss" src="./style.scss"></style>
<script type="text/javascript">
  /**
   * @component Img
   * @description
   *
   * ## 其他 / Img图片加载组件
   *
   * ### 介绍
   *
   * Img组件, 用于实现Img按需加载的功能. 当滚动到将要显示的位置的时候再加载图片
   *
   * ### 注意
   *
   * - Img组件**只能在Content组件**中使用!
   * - 当滚动到确定位置后, img未下载完但又滚动到别处, 这样的请求也会被中断
   * - 当图片加载完毕, 滚动超过阈值则隐藏不显示, 这样做是为了降低内存使用
   *
   * ### 如何引入
   * ```
   * // 引入
   * import { Img } from 'vimo'
   * // 安装
   * Vue.component(Img.name, Img)
   * // 或者
   * export default{
   *   components: {
   *    Img
   *  }
   * }
   * ```
   *
   * @props {String} [alt='image']         - 图片的alt属性
   * @props {(Number|String)} [height=0]   - 图片的高度
   * @props {String} src                   - 图片的src地址
   * @props {(Number|String)} [width=0]    - 图片的宽度
   *
   * @demo #/img
   * @usage
   * <Img width="100%" height="200" src="static/1.jpg">
   *
   * */
  import { isPresent } from '../../util/type'
  import registerListener from '../../util/register-listener'

  export default {
    name: 'Img',
    inject: {
      contentComponent: {
        from: 'contentComponent',
        default: null
      }
    },
    props: {
      src: String,
      alt: {
        type: String,
        default: 'image'
      },
      width: {
        type: [Number, String],
        default: 0
      },
      height: {
        type: [Number, String],
        default: 0
      }
    },
    data () {
      return {
        isLoaded: false,        // 判断DOM是否显示img
        srcValue: null,         // 内部使用的src值
        w: this.wQ,             // 当前渲染的尺寸值
        h: this.hQ,             // 当前渲染的尺寸值

        canRequest: false,      // 这个值是由content组件控制的!
        canRender: false,       // 这个值是由content组件控制的!

        hasLoaded: false,       // 判断图片是否真正下载完毕
        requestingSrc: null,    // 当前正在请求的src
        renderedSrc: null,      // 已经下载完毕渲染完毕的src
        rect: null,             // 当前组件与页面的位置关系
        imgElement: null,       // img标签元素
        wQ: this.getUnitValue(this.width) || 0,  // 记录最新的尺寸值
        hQ: this.getUnitValue(this.height) || 0, // 记录最新的尺寸值

        unRegLoadImg: null      // {function} 解除当前的注册事件
      }
    },
    watch: {
      width () {
        this.setDims()
      },
      height () {
        this.setDims()
      },
      src () {
        this.initSrcValue()
      }
    },
    methods: {
      /**
       * 组件初始化操作
       * @private
       * */
      init () {
        // console.assert(this.contentComponent, 'Img组件必须在Content组件中才能正常工作!')

        // 获取img元素
        this.imgElement = this.$refs.img

        // 设置组件的尺寸
        this.setDims()

        // 根据src初始化部分参数
        this.initSrcValue()

        this.contentComponent && this.contentComponent.$_addImg(this)
      },

      /**
       * 重置src请求,将img的src置空, 撤去渲染结果
       * 当前组件的src记录在this.src中, 如果未加载, 则将this.src=>this.requestingSrc, 表示图片需要下载.
       * @private
       * */
      reset () {
        if (this.requestingSrc && !this.renderedSrc && !this.hasLoaded) {
          // 图片在请求下载阶段, 但是还未下载完毕, 这时就直接断掉下载过程
          // console.warn(`abortRequest ${this.requestingSrc} ${Date.now()}`);
          this.srcAttr('')
          this.requestingSrc = null
        }
        if (this.renderedSrc) {
          // 当图片已经渲染出来显示过了, 则将其隐藏就行了, 这样做是为了降低内存使用
          // console.warn(`hideImg ${this.renderedSrc} ${Date.now()}`);
          this.setLoaded(false)
        }
      },

      /**
       * 更新
       * @private
       * */
      update () {
        // 图片的更新需要受到Content组件的控制 => Img组件的canRequest和canRender两个值
        if (this.src && this.contentComponent && this.contentComponent.$_isImgUpdatable()) {
          if (this.canRequest && (this.src !== this.renderedSrc && this.src !== this.requestingSrc) && !this.hasLoaded) {
            // 图片没请求过也没下载过页面渲染过的情况
            // 对img元素监听load事件, 事件结束解绑
            if (!this.unRegLoadImg) {
              this.unRegLoadImg = registerListener(this.imgElement, 'load', () => {
                this.unRegLoadImg && this.unRegLoadImg()
                this.unRegLoadImg = null
                this.hasLoaded = true // img loaded success!
                this.update()
              }, {passive: true})
            }

            // 第一次加载图片,先下载吧
            this.requestingSrc = this.src
            this.setLoaded(false)
            this.srcAttr(this.src)
            // 更新图片的尺寸
            this.setDims()
          }

          if (this.canRender && this.hasLoaded) {
            if (this.src !== this.renderedSrc) {
              // 已经下载但是从未显示的情况, 第一次显示
              this.renderedSrc = this.src
              // 更新图片的尺寸
              this.setDims()
              this.srcAttr(this.src)
              this.setLoaded(true)
            } else {
              // 已经下载过, 也显示过
              this.setLoaded(true)
            }
          }
        }
      },

      /**
       * 设置DOM控制
       * @private
       * */
      setLoaded (isLoaded) {
        this.isLoaded = isLoaded
      },

      /**
       * 设置imgbox的尺寸值
       * @private
       * */
      setDims () {
        // 发生变化才更新
        this.wQ = this.getUnitValue(this.width) || 0
        this.hQ = this.getUnitValue(this.height) || 0

        if (this.w !== this.wQ || this.h !== this.hQ) {
          if (this.w !== this.wQ) {
            this.w = this.wQ
          }
          if (this.h !== this.hQ) {
            this.h = this.hQ
          }
        }
      },

      /**
       * 初始化/改变prop中src值时的处理函数
       * @private
       * */
      initSrcValue () {
        // 重置src请求,将img的src置空, 撤去渲染结果
        this.reset()
        if (this.src.indexOf('data:') === 0) {
          // 如果使用的是datauri, 则意味着图片已经下载完毕
          this.hasLoaded = true
        } else {
          // 普通的src连接, 意味着图片还未下载
          this.hasLoaded = false
        }
        this.update()
      },

      /**
       * 真正设置img元素src的函数, 设置意味着即将进行img下载
       * @param {string} srcAttr - 将要加载的img的src属性值
       * @private
       * */
      srcAttr (srcAttr) {
        this.srcValue = srcAttr
      },

      /**
       * 将传入的height和width转为正确格式
       * @param {any} val
       * @return {string}
       * @private
       */
      getUnitValue (val) {
        if (isPresent(val)) {
          if (typeof val === 'string') {
            if (val.indexOf('%') > -1 || val.indexOf('px') > -1) {
              return val
            }
            if (val.length) {
              return val + 'px'
            }
          } else if (typeof val === 'number') {
            return val + 'px'
          }
        }
        return ''
      },

      /**
       * 获取当前组件的尺寸及距离页面的位置
       * @private
       * */
      getBounds () {
        if (!this.rect) {
          // 需要等待DOM更新完毕
          this.rect = this.$el.getBoundingClientRect()
        }
        return this.rect
      },

      /**
       * 获取从图片底部到页面顶部的距离
       * @private
       * */
      getBottom () {
        const bounds = this.getBounds()
        return (bounds && bounds.bottom) || 0
      },

      /**
       * 获取从图片顶部到页面顶部的距离
       * @private
       * */
      getTop () {
        const bounds = this.getBounds()
        return (bounds && bounds.top) || 0
      }
    },
    mounted () {
      this.init()
    },
    destroyed () {
      this.unRegLoadImg && this.unRegLoadImg()
      this.unRegLoadImg = null
      this.reset()
    }
  }
</script>