Vue图片懒加载
前言
图片懒加载大家都很熟悉了,优点如下:
- 提升页面渲染效率,避免大量图片加载造成页面卡顿。
- 按需加载,减少无效的图片加载,节约网络资源。
本文将使用vue自定义指令的方式,实现图片懒加载的功能。要点如下:
先来围观下,不使用图片懒加载功能,图片列表交互效果:
嗯,因为页面会同时请求全部的图片。如果不处理, 一是页面会比较卡,二是滚动到后边会出现白屏,用户体验非常差。接下来,主角登场 —— 图片懒加载指令他来了。
mock
首先使用 mock.js,准备一些图片数据。
目录地址: src/mock/index.js
import Mock from 'mockjs'
Mock.mock('/images', 'get', {
code: 200,
msg: 'ok',
'list|20': [
{
id: '@id',
'cover|1': [
"@image('300x200', @color, #fff, @title)"
]
}
]
})
当请求数据时,返回的数据格式大概是下面的样子:
此时图片数据他来了。
eventBus
图片交互中,我们需要监听图片容器的滚动,这里利用Vue内置的事件通知机制,封装了一个eventBus。该模块保证 eventBus既可以在所有组件内使用,也可以在普通的JS模块里使用。代码如下:
目录地址:src/eventBus.js
import Vue from 'vue'
const bus = new Vue()
Vue.prototype.$bus = bus
export default bus
复制代码
列表组件目录地址: src/components/List.vue
<template>
<div class="container" ref="container">
<ul>
<li v-for="(image, idx) in images" :key="image.id">
<img :src="image.cover" alt="" :data-id="idx"/>
</li>
</ul>
</div>
</template>
<script>
import { getImages } from "@/api/images";
export default {
data() {
return {
images: []
};
},
async created() {
const { data } = await getImages();
this.images = Object.freeze(data.list);
},
mounted() {
this.$refs.container.addEventListener("scroll", () => {
this.$bus.$emit("iscroll", this.$refs.container);
});
},
};
</script>
上面,我们在created阶段,请求到了mock的图片数据,并且在模板里循环遍历了图片列表。同时,在mounted阶段,我们通过监听图片容器的滚动,当滚动触发时,通过封装好的事件总线(eventBus)发布 “iscroll”
的事件通知。滚动监听他来了。
eventBus的更多了解,这里。
debounce
图片懒加载的机制是:当图片列表滚动时,动态的将可视区内的图片加载并渲染出来。但是,当滚动发生时,对图片进行展示的逻辑并非实时执行,而是等到滚动停止后的某个时间点执行,这样就可以避免在滑动过程中触发非必要的图片加载和渲染。所以,debounce防抖处理他来了。
/**
* 防抖函数
*
* @export
* @param {*} fn
* @param {*} delay
* @returns
*/
export function debounce(fn, delay) {
let timer = null
return function (...arg) {
clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arg)
}, delay);
}
}
关于防抖和节流,感兴趣的朋友,这里。
v-lazy
对Vue自定义指令不熟悉的朋友,这里。
到目前位置,List组件内的渲染时,会将所有的图片原地址添加到src属性上 —— :src="image.cover"
。这样,在组件渲染时,全部的图片都会加载和渲染。为了实现图片懒加载的效果,我们需要对src的赋值通过自定义指令做一层代理,并且指令的逻辑单独维护。
最新List组件实现,如下:
<template>
<div class="container" ref="container">
<ul>
<li v-for="(image, idx) in images" :key="image.id">
<img v-lazy:[container]="image.cover" alt="" :data-id="idx"/>
</li>
</ul>
</div>
</template>
<script>
//...
export default {
data() {
return {
images: [],
container: 'container' //图片容器的className
};
},
//...
};
</script>
上面,我们通过 v-lazy:[container] = ”image.cover“
来接管每个图片的原始地址,其中 container
为父容器的类名,在指令里判断图片是否在可视区会用到父容器,并且可以在data中动态指定。接下来,我们只需要关注 v-lazy
的实现即可。
另外,需要提前准备一张占位图片,每个图片默认开始时使用的是占位图。
目录地址:src/directives/lazy/img/cover-def.gif
v-lazy
指令实现细节如下:
imgs
首先,我们需要声明一个变量 imgs,用来存放当前待被懒加载的图片信息集合。当图片滚动发生时,遍历imgs,并找出在可视区停留的图片进行加载和渲染。
// 用来收集需要懒加载的图片信息
let imgs = []
指令配置
根据指令的写法,我们需要用到两个钩子函数:
- inserted:在绑定了该指令的图片被插入后,获取当前图片元素——
el
和 绑定的信息 ——binding
。需要处理逻辑包括:- 将占位图赋值给当前图片
- 通过
binding.arg
获取到指令参数 —— 图片容器的className,然后找到父容器 - 构建当前图片对应的图片信息对象img,包括:el(图片元素),src(图片原地址),containerClientHeight(图片容器的clientHeight值),并将img添加到imgs中。
- 立即处理当前图片是否需要加载和渲染 —— loadImg(这部分逻辑在单独函数里维护,后边详细讲)
- unbind:在图片指令解除绑定后,将图片从带加载的图片集合imgs里清除。
import defImage from './img/cover-def.gif'
// 用来收集需要懒加载的图片信息
let imgs = []
// 指令配置对象
const lazyDirective = {
inserted(el, binding) {
const { value, arg } = binding
let container = el.parentNode
// 设置默认图片
el.src = defImage;
// 找到容器
while (container && container.className.indexOf(arg) == -1) {
container = container.parentNode
}
const img = {
el: el,
src: value,
container: {
top: container && container.getBoundingClientRect().top || 0,
clientHeight: container && container.clientHeight || 0
},
height: el.height || 0
}
// 添加到当前需要被懒加载的图片集合内,当滚动时,通过加载真实图片
imgs.push(img)
// 立即处理:是否加载真实图片
loadImg(img)
},
unbind(el) {
// 当前图片的指令解除绑定时,需要从imgs中删除(因为此时当前图片已经销毁)
deleteImg(el)
}
}
iscroll 事件订阅
通过事件总线,注册 ”iscroll“事件,当 List 组件中的图片容器发生滚动时,会触发图片列表查看及加载判断。
import { debounce } from '@/utils'
import eventBus from '@/eventBus'
/**
* 查看所有待加载的图片
*
*/
function loadImages() {
for (const img of imgs) {
loadImg(img)
}
}
// 监听滚动
eventBus.$on('iscroll', debounce(loadImages, 300))
loadImg
通过 loadImg 函数处理每个带加载图片,是否加载的条件:图片是否在图片容器可视区内。
是否在可视区,分两种情况判断:
- 超出顶部可视区
- 超出底部可视区
来围观下示意图:
上图中的,disTop 大于 负的img.height 或 disTop 小于 container.clientHeight时,当前图片就在container的可视区域内。反之,就不在可视区。细品~ 。实现如下:
/**
* 加载真实图片
*
* @param {*} img
*/
function loadImg(img) {
const { el, src, height, container } = img
img.top = el.getBoundingClientRect().top || 0
const disTop = img.top - container.top
// 不在可视区
if (disTop < -height || disTop > container.clientHeight) return
// 在可视区
const image = new Image()
image.onload = () => {
el.src = src
}
image.src = src
console.log('加载了', el.dataset.id)
// 加载过的图片,需要将其从imgs中清除
deleteImg(el)
}
复制代码
另外,观察上面代码最后行,需要注意的是:加载过的图片,需要将其从imgs中清除。
deleteImg
从imgs中清除已经加载完成的img,实现如下:
/**
* 删除imgs中已加载的真实图片
*
* @param {*} el
*/
function deleteImg(el) {
imgs = imgs.filter(img => img.el != el)
}
复制代码
全局指令注册
为了保证改指令可以在全局内使用,需要将其注册为Vue的全局指令。实现如下:
import Vue from 'vue'
// 注册全局指令
Vue.directive('lazy', lazyDirective)
// 导出指令配置
export default lazyDirective
复制代码
完整实现
目录地址:src/directives/lazy/index.js
import Vue from 'vue'
import { debounce } from '@/utils'
import defImage from './img/cover-def.gif'
import eventBus from '@/eventBus'
// 用来收集需要懒加载的图片信息
let imgs = []
// 指令配置对象
const lazyDirective = {
inserted(el, binding) {
const { value, arg } = binding
let container = el.parentNode
// 设置默认图片
el.src = defImage;
// 找到容器
while (container && container.className.indexOf(arg) == -1) {
container = container.parentNode
}
const img = {
el: el,
src: value,
container: {
top: container && container.getBoundingClientRect().top || 0,
clientHeight: container && container.clientHeight || 0
},
height: el.height || 0
}
// 添加到当前需要被懒加载的图片集合内,当滚动时,通过加载真实图片
imgs.push(img)
// 立即处理:是否加载真实图片
loadImg(img)
},
unbind(el) {
// 当前图片的指令解除绑定时,需要从imgs中删除(因为此时当前图片已经销毁)
deleteImg(el)
}
}
/**
* 查看所有待加载的图片
*
*/
function loadImages() {
for (const img of imgs) {
loadImg(img)
}
}
/**
* 加载真实图片
*
* @param {*} img
*/
function loadImg(img) {
const { el, src, height, container } = img
img.top = el.getBoundingClientRect().top || 0
const disTop = img.top - container.top
// 不在可视区
if (disTop < -height || disTop > container.clientHeight) return
// 在可视区
const image = new Image()
image.onload = () => {
el.src = src
}
image.src = src
console.log('加载了', el.dataset.id)
// 加载过的图片,需要将其从imgs中清除
deleteImg(el)
}
/**
* 删除imgs中已加载的真实图片
*
* @param {*} el
*/
function deleteImg(el) {
imgs = imgs.filter(img => img.el != el)
}
// 监听滚动
eventBus.$on('iscroll', debounce(loadImages, 300))
// 注册全局指令
Vue.directive('lazy', lazyDirective)
export default lazyDirective
复制代码
这样,我们就完成了图片懒加载Vue自定义指令的封装。下面来看下效果吧!
根据控制台输出,滚动结束后,只加载可视区内的图片。效果是不是好些了。
小结
本文比较详细讲述了如何一步步封装一个图片懒加载的Vue自定义指令。内容涉及比较多,防抖、事件总线、如果判断图片在可视区等。希望感兴趣的小伙伴可以自己试着操作一遍,相信一定会有更深入的体会和理解。