实现Vue懒加载指令v-lazy

前面我们手写了图片的懒加载(可以查看之前文章),这次我结合 Vue 的插件 API 来更好的实现懒加载的指令。

有关插件教程可以查看: Vue 插件

有关自定义指令相关教程可以查看:Vue 自定义指令,这里不再做赘述。

1.收集懒加载的元素

当元素绑定 v-lazy 指定时,我们将该元素收集到 markElement数组中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
export const lazy = {
// v-lazy 绑定的元素
markElement: [],
// 收集元素
mark(el) {
this.markElement.push(el);
},

install(app) {
const _this = this;
app.directive("lazy", {
beforeMount(el, binding) {
_this.mark(el);
},
});
},
};

2.判断元素是否在可视区域

我们已经收集到了所有元素,那么我们就可以判断这些元素是否在可视区域了。判断逻辑还是和之前一样:监听页面滚动,当元素距离页面顶部的高度 top 小于 可视窗口的高度 viewHeight 时,元素即在可视范围内,加载图片:

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
// 防抖
debounce(fn, delay) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
}


// 判断元素是否在可视范围内,范围内的元素加载图片
init() {
this.markElement.forEach(el => {
const src = el.getAttribute("data-src");
if (!src) return;
const viewHeight = document.documentElement.clientHeight;
const top = el.getBoundingClientRect().top;
const bottom = el.getBoundingClientRect().bottom;
if (top < viewHeight && bottom > 0) {
el.setAttribute("src", src);
}
});
}


install(app) {
app.directive("lazy", {...})
// 监听页面滚动
window.addEventListener("scroll", this.debounce(() => {
this.init();
}, 200));
}

install 中注册滚动事件,并去判断所有收集的元素是否在可视范围内,而不是在指定的生命周期中注册 scroll 事件,避免重复注册,造成性能浪费。

别忘了还可以使用构造函 IntersectionObserver 实现,这里我们判断浏览器支持就是用该 api,不支持则使用“滚动判断法”:

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
 observer() {
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
// 无data src 属性,不处理
if (entry.isIntersecting && entry.target.getAttribute("data-src")) {
entry.target.setAttribute("src", entry.target.getAttribute("data-src"));
observer.unobserve(entry.target);
}
});
}, {
root: null,
threshold: .3
});
return observer;
}


// 支持 IntersectionObserver 则使用 IntersectionObserver 加载图片
if (IntersectionObserver) {
const observer = _this.observer();
_this.markElement.forEach(el => {
observer.observe(el);
});
}

// 不支持 IntersectionObserver则使用 scroll 事件加载图片
if (!IntersectionObserver) {
window.addEventListener("scroll", this.debounce(() => {
this.init();
}, 200));
}

3.初始化

在初次进入页面时我们需要初始化一次判断元素是否在可视范围内,使用 IntersectionObserver时,也需要初始化监听所有元素,这里可以在 mounted 声明周期中实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
install(app) {
const _this = this;
app.directive("lazy", {
beforeMount(el, binding) {
_this.mark(el);
},
mounted() {

// 支持 IntersectionObserver 则使用 IntersectionObserver 加载图片
if (IntersectionObserver) {
const observer = _this.observer();
_this.markElement.forEach(el => {
observer.observe(el);
});
} else {
// 不支持 使用手写代码
_this.init();
}
}
})
}

下面是完整代码:

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
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
export const lazy = {
// v-lazy 绑定的元素
markElement: [],
// 收集元素
mark(el) {
this.markElement.push(el);
},
// 判断元素是否在可视范围内,范围内的元素加载图片
init() {
this.markElement.forEach((el) => {
const src = el.getAttribute("data-src");
if (!src) return;
const viewHeight = document.documentElement.clientHeight;
const top = el.getBoundingClientRect().top;
const bottom = el.getBoundingClientRect().bottom;
if (top < viewHeight && bottom > 0) {
el.setAttribute("src", src);
}
});
},
// 防抖
debounce(fn, delay) {
let timer = null;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(() => fn.apply(this, arguments), delay);
};
},
observer() {
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting && entry.target.getAttribute("data-src")) {
entry.target.setAttribute("src", entry.target.getAttribute("data-src"));
observer.unobserve(entry.target);
}
});
},
{
root: null,
threshold: 0.3,
}
);
return observer;
},
install(app) {
const _this = this;
app.directive("lazy", {
beforeMount(el, binding) {
_this.mark(el);
},
mounted() {
// 支持 IntersectionObserver 则使用 IntersectionObserver 加载图片
if (IntersectionObserver) {
const observer = _this.observer();
_this.markElement.forEach((el) => {
observer.observe(el);
});
} else {
// 不支持 使用手写代码
_this.init();
}
},
});
// 不支持 IntersectionObserver则使用 scroll 事件加载图片
if (!IntersectionObserver) {
window.addEventListener(
"scroll",
this.debounce(() => {
this.init();
}, 200)
);
}
},
};

实现效果:

参考:


实现Vue懒加载指令v-lazy
https://www.zphl.top/posts/vue-v-lazy.html
作者
lzp
发布于
2024年6月16日
许可协议