用vue写一个简单的图片懒加载指令(移动端)

628 阅读3分钟

前言

在移动端,图片懒加载是一个常用的优化手段,而且在vue项目中,也已经存在类似的插件(比如:v-lazy)。但是,如果您想自己动手写一个类似的指令的话,那看这篇文章就对了,笔者会介绍一个炒鸡简单的实现懒加载方法。

必杀技 IntersectionObserver

现在就儆上本篇文章的关键知识点IntersectionObserver.

定义:IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。祖先元素与视窗(viewport)被称为根(root)。(--MDN)

IntersectionObserver 会 监听特定的元素有没有在给定的比例中出现在可见区域,替代了传统的 滚动监听+getBoundingClientRect 获取元素位置。 使用方法也很简单,先 new 一个实例(new IntersectionObserver),然后在构造函数中的回调函数里执行逻辑代码,最后使用observe方法添加要监听的元素。

// 实例化
let observer = new IntersectionObserver((changes)=>{
	changes.forEach((change)=>{
      let target = change.target;
      if(change.intersectionRatio>0){
     	// 元素出现在可视区域了, 下面写逻辑代码
        ...
        observer.unobserve(target); // 记得解绑
      }
    })
})

// 添加监听对象
observer.observe(el);

结合vue编写自定义指令

首先,根据vue官方的例子,写一个自定义指令的模板出来:

// 注册一个全局自定义指令 `v-load`
Vue.directive("load", {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function(el, binding) {
    console.log('inserted!!!');
  },
  update: function(el, binding) {
    console.log('update!!!');
  },
  unbind: function(el){ // 
    console.log('unbind !!!');
   
  }
});

接下来,封装一下 IntersectionObserver 的初始化,代码跟上面的实例化差不多:

// 封装
function Observer(){
  let observer = new IntersectionObserver((changes)=>{
    changes.forEach((change)=>{
      let target = change.target;
      if(change.intersectionRatio>0){
        //todo: 设置图片路径
        observer.unobserve(target);
      }
    })
  },{
    threshold: [ 0.25] // 0~1 相对自身的比例
  });
  return observer;
}

// 判断浏览器是否支持
let observer = null;
if(window.IntersectionObserver){
  observer = Observer();
}

// 然后在指令 inserted的时候 添加要监听的元素
inserted: function(el, binding) {
    console.log('inserted!!!');
    el.binding = binding;
    
    if(window.IntersectionObserver){
      observer.observe(el);
    } 
  },

下面就来写一下设置图片的代码:

/**
* el 使用指令的元素
* binding vue指令钩子函数的参数
* placeholder 是否显示默认图片(占位符)
*/
function setImg(el, binding,placeholder=false){
	let defaultImg = 'base64:xxxxx'; //可以使用base64格式,或者是require("xxx")图片
    if(placeholder){
    	el.src = defaultImg;
    }
    
    if (!binding.value) {
      el.src = defaultImg;
    } else {
      let img = new Image();
      img.src = binding.value;
      img.onload = function() {
        el.src = binding.value;
        img = img.onload = null;
      };
      img.onerror = function() {
        el.src = defaultImg;
        img = img.onerror = null;
      };
   }
}

// 使用
...
if(change.intersectionRatio>0){
    //设置图片路径
    setImg(target,target.binding,change.intersectionRatio);
    observer.unobserve(target);
  }

// 在vue文件中使用指令
<img v-load="xxx"/>

到这来你就已经写出一个懒加载的指令,是不是很简单。什么? 这么好的属性你担心有没有兼容问题? 嗯,我查一下,额,有些浏览器还真的不兼容(乐色):

降级处理

如果担心兼容问题的话,没关系,咱们还能做个降级处理的,使用 滚动监听+获取元素位置信息 判断是否在可视范围内。

下面给出关键的代码:

//常量
const W_HEIGHT = window.innerHeight;
const SCALE = 0.25; //高度比例 0~1
const RANGE = 10; // 滚动距离误差(px)

//降级处理,不支持 IntersectionObserver 的 使用touch+getBoundingClientRect
function scrollToRect(){
  let startY=0;
  document.removeEventListener('touchstart',touchStartHandler);
  document.removeEventListener('touchend',touchEndHandler);
  document.addEventListener('touchstart',touchStartHandler);
  document.addEventListener('touchend',touchEndHandler);

  function touchStartHandler(e){
    startY = e.changedTouches[0].pageY;
  }

  function touchEndHandler(e){
    console.log(e);
    let endY = e.changedTouches[0].pageY;
    if(endY<startY && Math.abs(endY - startY)>RANGE){
      console.log('向下滑动了',Math.abs(endY - startY));
      traverseList();
    }
  }
}

function traverseList(){
  for(let img of imgList){
    if(img.loaded) continue;
    reactByItem(img);
  }
}

function reactByItem(item){
  const rect = item.getBoundingClientRect();
    if(rect.height*SCALE+rect.top < W_HEIGHT){
      // 进入屏幕
      console.log('我进入屏幕了,哈哈哈');
      item.loaded = true;
      setImg(item,item.binding,item.loaded);
    }
}

下面贴一下整体的代码(拿走不谢):


import Vue from "vue";
let observer = null;
if(window.IntersectionObserver){
  observer = Observer();
}else{
  scrollToRect();
}

// 常量
const W_HEIGHT = window.innerHeight;
const SCALE = 0.25; //高度比例 0~1
const RANGE = 10; // 滚动距离误差(px)


let imgList = [];

// 注册一个全局自定义指令 `v-load`
Vue.directive("load", {
  // 当被绑定的元素插入到 DOM 中时……
  inserted: function(el, binding) {
    console.log('inserted!!!');
    el.binding = binding;
    
    if(window.IntersectionObserver){
      observer.observe(el);
    }else{
      el.loaded = false;
      imgList.push(el);
      reactByItem(el);
    }
    
    
  },
  update: function(el, binding) {
    if(el.loaded){
      setImg(el, binding);
    }
  },
  unbind: function(el){ // 
    console.log('unbind !!!');
    imgList = []; //移除的时候清空
  }
});

function setImg(el, binding,placeholder=false) {
  
  let defaultImg = require("xxxx");

  if(placeholder){
    el.src = defaultImg; //默认显示加载图片
  }
  if (!binding.value) {
    el.src = defaultImg;
  } else {
    let img = new Image();
    img.src = binding.value;
    img.onload = function() {
      el.src = binding.value;
      img = img.onload = null;
    };
    img.onerror = function() {
      el.src = defaultImg;
      img = img.onerror = null;
    };
  }
}
/**
 * 利用 IntersectionObserver 替代 onscroll+getBoundingClientRect实现定位懒加载
 */
function Observer(){
  let observer = new IntersectionObserver((changes)=>{
    // console.log(changes);
    changes.forEach((change)=>{
      let target = change.target;
      if(change.intersectionRatio>0){
        setImg(target,target.binding,change.intersectionRatio);
        observer.unobserve(target);
      }
    })
  },{
    threshold: [ 0.25]
  });
  return observer;
}

// 降级处理,不支持 IntersectionObserver 的 使用touch+getBoundingClientRect
function scrollToRect(){
  let startY=0;
  document.removeEventListener('touchstart',touchStartHandler);
  document.removeEventListener('touchend',touchEndHandler);
  document.addEventListener('touchstart',touchStartHandler);
  document.addEventListener('touchend',touchEndHandler);

  function touchStartHandler(e){
    startY = e.changedTouches[0].pageY;
  }

  function touchEndHandler(e){
    let endY = e.changedTouches[0].pageY;
    if(endY<startY && Math.abs(endY - startY)>RANGE){
      console.log('向下滑动了',Math.abs(endY - startY));
      traverseList();
    }
  }
}

function traverseList(){
  for(let img of imgList){
    if(img.loaded) continue;
    reactByItem(img);
  }
}

function reactByItem(item){
  const rect = item.getBoundingClientRect();
    if(rect.height*SCALE+rect.top < W_HEIGHT){
      // 进入屏幕
      console.log('我进入屏幕了,哈哈哈');
      item.loaded = true;
      setImg(item,item.binding,item.loaded);
    }
}