自定义指令实现图片懒加载
灵感胜于汗水 Lv5

vue-lazyload

main.js中引入vue-lazyload:

1
2
3
4
5
6
7
8
9
10
import Vue from 'vue'
import App from './App.vue'

import VueLazyload from "vue-lazyload";

Vue.use(VueLazyload,{
loading:'http://localhost:3000/img/loading.gif', //图片加载中显示的图片
error:'http://localhost:3000/img/error-img.png', //图片加载错误显示的图片
preLoad:1 //超出1倍屏幕高度的图片先不加载
})

.vue文件中使用:

使用指令v-lazy代替img标签的src属性,表示该图片使用懒加载

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<template>
<div id="app">
<div v-for="(item,index) in imgData" :key="index">
<div class="img">
<img v-lazy="item.img" alt="img">
</div>
<div class="content">{{item.name}}</div>
</div>
</div>
</template>

<script>
import axios from 'axios'

export default {
name: 'App',
data() {
return {
imgData: []
}
},
mounted() {
this.getImgs()
},
methods: {
async getImgs() {
const res = await axios.get('http://localhost:3000/imgs')
this.imgData = res.data
}
}
}
</script>

自定义指令实现v-lazy

手写插件vue-lazyload

新建文件modules/vue-lazyload/index.js:

1
2
3
4
5
export default { //默认暴露一个带有install方法的对象
install(Vue,options){ //Vue:Vue构造器,options:使用插件时传入的配置对象

}
}

main.js中引入自己的插件:

1
import VueLazyload from "./modules/vue-lazyload";

自定义指令

1
2
3
4
5
6
7
8
9
10
11
12
13
export default {
install(Vue, options) {
//自定义指令,参数一:自定义指令名,参数二:定义该指令功能的对象
Vue.directive('lazy', {
//指令定义对象可以调用一些钩子函数:比如bind、inserted
//钩子函数的参数,el:指令所绑定的元素,binding:一个对象,vnode:虚拟节点
//bind:只调用一次,指令第一次绑定到元素时调用
bind(el,binding,vnode){

}
})
}
}

功能实现的类

接下来就需要在bind钩子函数中实现功能逻辑,为了更好的扩展性,将功能封装成一个类:

创建modules/vue-lazyload/lazy.js

1
2
3
4
5
6
7
8
9
10
11
12
export default function (Vue) { //暴露一个函数,接收Vue构造器
return class Lazy { //返回一个类,接收options
constructor(options) {
this.options = options
}

//实现功能的函数
bindLazy(el, binding) {

}
}
}

modules/vue-lazyload/index.js:

1
2
3
4
5
6
7
8
9
10
11
import lazy from './lazy' //导入lazy.js

export default {
install(Vue, options) {
const LazyClass = lazy(Vue)
const lazyload = new LazyClass(options)
Vue.directive('lazy', {
bind: lazyload.bindLazy.bind(lazyload) //绑定函数,注意修改this指向
})
}
}

准备一个函数,用于获取dom的最近的滚动父节点(overflow:scroll)

创建modules/vue-lazyload/util.js:

1
2
3
4
5
6
7
8
9
10
11
export function getScrollParent(el) {
let _parent = el.parentNode
while (_parent) {
//getComputedStyle:获取目标的所有css属性
const overflow = getComputedStyle(_parent)['overflow'] //获取overflow属性值
if (/(scroll)|(auto)/.test(overflow)) {
return _parent
}
_parent = _parent.parentNode
}
}

lazy.js中使用Vue.nextTick:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import {getScrollParent} from './util'

export default function (Vue) {
return class Lazy {
constructor(options) {
this.options = options
this.isAddScrollListener = false
}

bindLazy(el, binding) {
Vue.nextTick(() => {
const scrollParent = getScrollParent(el)
if (scrollParent && !this.isAddScrollListener) { //如果还没有绑定事件
scrollParent.addEventListener('scroll',this.handleScroll.bind(this))
}
})
}

//滚动事件
handleScroll(){

}
}
}

图片实例的类

创建modules/vue-lazyload/lazyimg.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default class Lazyimg {
constructor({el, src, options, imgRender}) {
this.el = el
this.src = src
this.options = options
this.imgRender = imgRender
this.loaded = false //已经加载过
this.state = {
loading: false, //加载成功
error: false //加载失败
}
}
i
//图片是否在指定范围内
checkIsVisible() {
const {top} = this.el.getBoundingClientRect() //获取元素距顶部的距离
return top < window.innerHeight * (this.options.preLoad || 1.3) //判断是否在范围内,preLoad默认1.3
}

//加载图片(未完成,第二个参数先写死成loading,参数一为该图片实例)
loadImg() {
this.imgRender(this,'loading') //加载图片,图片加载中时显示的图片
}
}

lazy.js:每次触发bind时创建一个图片实例,保存到数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import {getScrollParent} from './util'
import {throttle} from 'lodash' //节流
import Lazyimg from "./lazyimg";

export default function (Vue) {
return class Lazy {
constructor(options) {
this.options = options
this.isAddScrollListener = false
this.lazyimgPool = [] //图片实例数组
}

bindLazy(el, binding) {
Vue.nextTick(() => {
const scrollParent = getScrollParent(el)
if (scrollParent && !this.isAddScrollListener) {
scrollParent.addEventListener('scroll', throttle(this.handleScroll.bind(this), 200))
this.isAddScrollListener = true
}
//创建一个新的图片实例
const lazyimg = new Lazyimg({
el,
src: binding.value, //v-lazy指令绑定的值
options: this.options,
imgRender: this.imgRender.bind(this)
})
this.lazyimgPool.push(lazyimg)
this.handleScroll() //滚动事件在一开始就执行一次
})
}

//滚动事件
handleScroll() {

}

//图片渲染函数
imgRender() {

}
}
}

滚动事件

1
2
3
4
5
6
7
8
9
handleScroll() {
let isVisible = false
this.lazyimgPool.forEach(lazyimg => {
if (!lazyimg.loaded) { //图片还没有加载
isVisible = lazyimg.checkIsVisible() //图片是否出现在指定的范围内(perLoad指定的)
isVisible && lazyimg.loadImg() //如果出现在范围内,则加载图片
}
})
}

渲染图片

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
imgRender(lazyimg, state) {
const {el, options} = lazyimg
const {loading, error} = options
let src = ''
switch (state) {
case 'loading': //加载中
src = loading || ''
break
case 'error': //加载错误
src = error || ''
break
default: //加载完成,显示真正的目标图片
src = lazyimg.src
}
el.setAttribute('src',src) //设置或改变图片的src
}

渲染完成

util.js中添加:

1
2
3
4
5
6
7
8
export function imgLoad(src) {
return new Promise(((resolve, reject) => {
const oImg = new Image()
oImg.src = src
oImg.onload = resolve //加载成功
oImg.onerror = reject //加载失败
}))
}

lazy.js:

1
2
3
4
5
6
7
8
9
10
11
12
loadImg() {
this.imgRender(this, 'loading')
imgLoad(this.src).then(() => {//成功
this.state.loading = true
this.imgRender(this, 'ok')
this.loaded = true
}).catch(() => {//失败
this.state.error = true
this.imgRender(this, 'error')
this.loaded = true
})
}

通过IntersectionObserver实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
export default {
install(Vue, options) {
Vue.directive('lazy', {
bind(el, binding) {
init(el, binding.value, options.loading)
},
inserted(el) {
observer(el)
}
})
}
}

// 初始化
function init(el, src, loading) {
el.setAttribute('data-src', src)
el.setAttribute('src', loading)
}

// 利用IntersectionObserver监听el
function observer(el) {
let observer = new IntersectionObserver(entries => {
if (entries[0].isIntersecting){ //进入视口
let realSrc=el.dataset.src
if (realSrc){
el.setAttribute('src',realSrc)
el.removeAttribute('data-src')
}
}
})
observer.observe(el)
}

生成观察器实例:let observer = new IntersectionObserver(callback,option)

接收两个参数,callback:可见性变化时的回调函数,option:可选的配置项

callback:

一般会触发两次。一次是目标元素刚刚进入视口(开始可见),另一次是完全离开视口(开始不可见)。

1
2
3
4
5
let io = new IntersectionObserver(
entries => {
console.log(entries);
}
)

enteries:是一个数组,每个成员是IntersectionObserverEntry对象,如果同时有多个被观察的对象的可见性发生变化,enteries数组就有多个成员。

IntersectionObserverEntry对象的部分属性:

  • isIntersecting:是否可见
  • time:可见性发生变化的时间,是一个高精度时间戳,单位为毫秒
  • boundingClientRect:目标元素的矩形区域信息
  • intersectionRatio:目标元素的可见比例,完全可见时为1,完全不可见时小于等于0
  • intersectionRect:目标元素与视口(或根元素)的交叉区域的信息
  • target:目标元素
  • rootBounds:根元素的矩形区域的信息

option对象属性:

  • threshold:数组,决定何时触发回调函数,比如,[0, 0.25, 0.5, 0.75, 1]就表示当目标元素 0%、25%、50%、75%、100% 可见时,会触发回调函数。
  • rootrootMarginroot属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。rootMargin属性。后者定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。

参考文档

参考视频

  • 本文标题:自定义指令实现图片懒加载
  • 本文作者:灵感胜于汗水
  • 创建时间:2022-04-03 18:35:16
  • 本文链接:https://cjhsyc.github.io/2022/04/03/自定义指令实现图片懒加载/
  • 版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!