深入浅出vue-plugins-v-lazy

2,238 阅读4分钟

前言

Vue-lazy学习总结,学无止境,每天进步一点点,加油

应用层

  • 入口文件main.js
// vue-lazyload 图片懒加载 v-lazy
import Vue from 'vue';
import VuelazyLoad from './vue-lazyload';
import loading from './loading.jpg'
import App from './App.vue'
// use方法是一个全局的api 会调用 VuelazyLoad install
Vue.use(VuelazyLoad,{
    preLoad: 1.3, // 可见区域的1.3倍
    loading, // loading图
}); // use的默认调用就会执行VuelazyLoad的install方法

new Vue({
    el:'#app',
    render:h=>h(App)
})
  • 根组件App.vue
<template>
    <div class="box">
        <li v-for="img in imgs" :key="img">
            <img v-lazy="img">
        </li>        
    </div>
</template>
<script>
import axios from 'axios'; // 基于promise async + await
export default {
    data(){
        return {imgs:[]}
    },
     created(){ 
       axios.get('图片请求地址').then(({data})=>{
           
           this.imgs = data;
       })
     }
}
</script>
<style>
.box {
    height:300px;
    overflow: scroll;
    width: 200px;
}
img{
    width: 100px; height:100px;
}
</style>

可以看到,其实用法挺简单的,常规的插件use后,直接在img标签上加v-lazy就可以了,由此我们也可以猜测,其实v-lazy是一个“包着插件的皮,有着自定义指令实现的心”的存在;

个人梳理xmind

实现逻辑

前备知识
  • jsApi
    • getComputedStyle(dom) 获取dom的属性对象,本文用以得到overflow:scroll的节点
    • dom.getBoundingClientRect() 获取距离信息,本文用以获取img距离设备可视区顶部的高度值
  • Vue中use插件时,会默认执行插件的istall方法
  • 自定义指令的bind生命周期函数会被默认传递三个参数el、binging和vnode,建议官网了解
实现思路(注释是重点)
  • 插件的index入口文件简单的做下初始化调用add方法
    • vue-lazy默认导出一个高阶函数,调用返回一个类LazyClass,实例的add方法时重点逻辑

export let _Vue;
import Lazy from './lazy';
export default {
    <!--vue默认传递参数-->
    install(Vue,options){
        _Vue = Vue;
        const LazyClass = Lazy(Vue);
        const lazy = new LazyClass(options)
        Vue.directive('lazy',{
            bind: lazy.add.bind(lazy)
        })
    }
}
  • Vue中use插件时,会默认执行插件的istall方法,所以我们可以在istall中自定义全局指令,然后在指令的生命周期函数bind中写渲染逻辑
    • 注意:bind生命周期中dom还未挂载,所以有获取dom的操作必须包裹在nextTick里

  add(el, binding) {
      Vue.nextTick(() => {
        // 找到带有overflow属性的祖先元素
        function scrollParent() {
          let parent = el.parentNode;
          while (parent) {
            if (/scroll/.test(getComputedStyle(parent)["overflow"]))
              return parent;
            parent = parent.parentNode;
          }
        }
        let parent = scrollParent();
        let src = binding.value;
        // 监控带有overflow属性的祖先元素的滚动事件;滚动时则调用listener的校验状态等默认事件,是为发布
        let listener = new ReactiveListener({
          el,
          src,
          elRenderer: this.elRenderer.bind(this),
          options: this.options
        });
        // 将当前绑定的img对应的观测者保存到持有的数组中,是为订阅;
        this.listenerQueue.push(listener);
        // 因为每个img上的v-lazy都会执行add方法,所以都会尝试挂载scroll事件,但LazyClass实例的单例的,所以加这个参数做一个优化
        if(!this.isBinded){
            // 监控带有overflow属性的祖先元素的滚动事件
            parent.addEventListener("scroll", this.lazyLoadHandler);
        }
// 执行add方法时默认调用一次lazyLoadHandler方法
          this.lazyLoadHandler()
        
      });
    }
   
  • 滚动时,会触发lazyLoadHandler,是LazyClass上的属性,滚动事件处理函数
    • 只做一件事,遍历listenQueue,判断哪些img需要渲染
 //   throttle是loadsh的节流方法
      this.lazyLoadHandler = throttle(() => {
        //   用以判断img是否在可渲染区域内
        let catIn = false;
        this.listenerQueue.forEach(listener => {
            // 如果已经在加载,则不作处理
            if(listener.state.loading) return
            // 判断img是否在可渲染区域内
          catIn = listener.checkInView();
        //   如在,则执行加载
          catIn && listener.load();
        });
      })
  • img的观测者类的结构
  class ReactiveListener {
    constructor({ el, src, elRenderer, options }) {
      this.el = el;
      this.src = src;
      this.elRenderer = elRenderer;
      this.options = options;
      this.state = {};
    }
    // 判断是否渲染
    checkInView() {
      let {top} = this.el.getBoundingClientRect();
      console.log(top);
      
      return top < window.innerHeight * this.options.preLoad
    }
    load() {
        this.elRenderer(this,'loading');
        loadImageAsync(this.src,()=>{
            this.state.loading = true;
            this.elRenderer(this,'loading')
        },()=>{
            this.elRenderer(this,'error')
        });
    }
  
  • 实际渲染方法
  elRenderer(listener, state) {
      let { el } = listener;
      let src = "";
      switch (state) {
        case "loading":
          src = listener.options.loading || "";
          break;
        case "error":
          src = listener.options.error || "";
          break;

        default:
          src = listener.src;
          break;
      }
      el.setAttribute("src", src);
    }
整体实现思路

后语

看人家源码写的真是巧妙,自认菜鸟一枚,望掘金大神有错直指,虚心求教,有错必改; --19年毕业小菜语

个人简易版源码

# index.js

export let _Vue;
import Lazy from './lazy';
export default {
    install(Vue,options){
        _Vue = Vue;
        const LazyClass = Lazy(Vue);
        const lazy = new LazyClass(options)
        Vue.directive('lazy',{
            bind: lazy.add.bind(lazy)
        })
    }
}
# lazy.js

import {throttle} from 'lodash';
export default Vue => {
  class ReactiveListener {
    constructor({ el, src, elRenderer, options }) {
      this.el = el;
      this.src = src;
      this.elRenderer = elRenderer;
      this.options = options;
      this.state = {};
    }
    // 判断是否渲染
    checkInView() {
      let {top} = this.el.getBoundingClientRect();
      console.log(top);
      
      return top < window.innerHeight * this.options.preLoad
    }
    load() {
        this.elRenderer(this,'loading');
        loadImageAsync(this.src,()=>{
            this.state.loading = true;
            this.elRenderer(this,'loading')
        },()=>{
            this.elRenderer(this,'error')
        });
    }
  }
  function loadImageAsync(src,resolve,reject) {
      let image = new Image();
      image.src =  src;
      image.onload = resolve;
      image.onerror = reject;
  }
  return class LazyClass {
    constructor(options) {
      this.options = options;
      this.listenerQueue = [];
      this.bindHandler = false;
    //   throttle是loadsh的节流方法
      this.lazyLoadHandler = throttle(() => {
        //   用以判断img是否在可渲染区域内
        let catIn = false;
        this.listenerQueue.forEach(listener => {
            // 如果已经在加载,则不作处理
            if(listener.state.loading) return
            // 判断img是否在可渲染区域内
          catIn = listener.checkInView();
        //   如在,则执行加载
          catIn && listener.load();
        });
      })
     
    }
    add(el, binding) {
      Vue.nextTick(() => {
        // 找到带有overflow属性的祖先元素
        function scrollParent() {
          let parent = el.parentNode;
          while (parent) {
            if (/scroll/.test(getComputedStyle(parent)["overflow"]))
              return parent;
            parent = parent.parentNode;
          }
        }
        let parent = scrollParent();
        let src = binding.value;
        // 监控带有overflow属性的祖先元素的滚动事件;滚动时则调用listener的校验状态等默认事件,是为发布
        let listener = new ReactiveListener({
          el,
          src,
          elRenderer: this.elRenderer.bind(this),
          options: this.options
        });
        // 将当前绑定的img对应的观测者保存到持有的数组中,是为订阅;
        this.listenerQueue.push(listener);
        // 因为每个img上的v-lazy都会执行add方法,所以都会尝试挂载scroll事件,但LazyClass实例的单例的,所以加这个参数做一个优化
        if(!this.isBinded){
            // 监控带有overflow属性的祖先元素的滚动事件
            parent.addEventListener("scroll", this.lazyLoadHandler);
        }
// 执行add方法时默认调用一次lazyLoadHandler方法
          this.lazyLoadHandler()
        
      });
    }
    elRenderer(listener, state) {
      let { el } = listener;
      let src = "";
      switch (state) {
        case "loading":
          src = listener.options.loading || "";
          break;
        case "error":
          src = listener.options.error || "";
          break;

        default:
          src = listener.src;
          break;
      }
      el.setAttribute("src", src);
    }
  };
};