前面我们手写了图片的懒加载(可以查看之前文章),这次我结合 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 = { 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 => { 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; }
if (IntersectionObserver) { const observer = _this.observer(); _this.markElement.forEach(el => { observer.observe(el); }); }
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() {
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 = { 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() { if (IntersectionObserver) { const observer = _this.observer(); _this.markElement.forEach((el) => { observer.observe(el); }); } else { _this.init(); } }, }); if (!IntersectionObserver) { window.addEventListener( "scroll", this.debounce(() => { this.init(); }, 200) ); } }, };
|
实现效果:
参考: