前言
在移动端,图片懒加载是一个常用的优化手段,而且在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);
}
}