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 })
.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 (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 (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 ) { return class Lazy { 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' export default { install (Vue, options ) { const LazyClass = lazy (Vue ) const lazyload = new LazyClass (options) Vue .directive ('lazy' , { bind : lazyload.bindLazy .bind (lazyload) }) } }
准备一个函数,用于获取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) { const overflow = getComputedStyle (_parent)['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 ) } 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 , 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 () 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) }
渲染完成 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) } 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% 可见时,会触发回调函数。
root、rootMargin:root属性指定目标元素所在的容器节点(即根元素)。注意,容器元素必须是目标元素的祖先节点。rootMargin属性。后者定义根元素的margin,用来扩展或缩小rootBounds这个矩形的大小,从而影响intersectionRect交叉区域的大小。它使用CSS的定义方法,比如10px 20px 30px 40px,表示 top、right、bottom 和 left 四个方向的值。
参考文档
参考视频