一、说明与使用
Vue-Lazyload作为Vue框架的图片懒加载的插件,通过指令的方式在Vue开发中使用。
如下来自官方使用说明:
1 引入
import VueLazyload from 'vue-lazyload' // 引入
Vue.use(VueLazyload) // 不带参数注册指令
// or with options // 带参数注册指令
Vue.use(VueLazyload, {
preLoad: 1.3, // proportion of pre-loading height, 个人理解是图片加载前目标元素位置范围
error: 'dist/error.png', // 加载错误时
loading: 'dist/loading.gif', // 加载中
attempt: 1 // 下载图片时错误重连次数
//... 还可以设置更多参数,如触发事件
})
2 使用
<!-- 单个图片实现懒加载 ->
<ul>
<li v-for="img in list">
<img v-lazy="img.src" >
</li>
</ul>
<!-- 通过定义懒加载的图片容器,类似于事件委托 ->
<div v-lazy-container="{ selector: 'img' }">
<img data-src="//domain.com/img1.jpg">
<img data-src="//domain.com/img2.jpg">
<img data-src="//domain.com/img3.jpg">
</div>
二、原理探索
1. 核心思想
- 先将图片的src设置为同一张图片或者不设置,同时给img标签设置一个特殊属性,例如:data-src用于存放图片的真实预览地址;
- 若图片未进入可视区域时,展示同一张图片或者直接不展示图片,此时就不会发生http请求,当图片进入可视区域时,将data-src上的值赋给src,此时再发送http请求。
2 Vue-Lazyloader 懒加载的原理
- 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件);
- 上面的dom事件回调中,会遍历listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源;
- 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容
3. 核心代码分析
一下代码版本:1.3.3
constructor ({ preLoad, error, throttleWait, preLoadTop, dispatchEvent, loading, attempt, silent = true, scale, listenEvents, hasbind, filter, adapter, observer, observerOptions }) { // .... 其它属性 this.ListenerQueue = [] // 监控事件变化队列 this.TargetQueue = [] // 触发事件的目标元素的队列
this.options = {
throttleWait: throttleWait || 200, // 防抖时间
preLoad: preLoad || 1.3, // 可视范围倍数
preLoadTop: preLoadTop || 0,
error: error || DEFAULT_URL, // 错误图片地址
loading: loading || DEFAULT_URL, // 加载样式
attempt: attempt || 3,
ListenEvents: listenEvents || DEFAULT_EVENTS,
// .... 其它代码
}
this._initEvent()
this._imageCache = new ImageCache({ max: 200 })
// 懒加载函数
this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)
this.setMode(this.options.observer ? modeType.observer : modeType.event)
}
在Lazy构造函数中,初始化监控事件变化的队列,目标DOM队列,以及相关Option。
构造函数中,首先进行了事件注册,设置目标元素,定义懒加载函数lazyLoadHandler。
this.Event.listeners[event].push(func) // _initEvent
this.options.ListenEvents.forEach((evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler)) // _initListener
_initListen
其中:
DEFAULT_EVENTS 默认的触发的事件类型范围有:
'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
DEFAULT_URL 加载错误后的默认图片为,最小base64 的透明图片
'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'
Lazy load模块和listener模块他们的业务职责分工明确。
lazy负责和dom相关的处理,包括为dom创建listener,为target注册dom事件,渲染dom;listener只负责状态的控制,在不同状态执行不同的业务。
lazyLoadHandler方法
/** 发现那些图片节点在可是范围内,则触发load加载 * find nodes which in viewport and trigger load * @return */ _lazyLoadHandler () { const freeList = [] this.ListenerQueue.forEach((listener, index) => { if (!listener.el || !listener.el.parentNode) { freeList.push(listener) } const catIn = listener.checkInView() if (!catIn) return listener.load() }) freeList.forEach(item => { remove(this.ListenerQueue, item) item.$destroy() }) }
Load 方法
/* * try load image and render it * @return */ load (onFinish = noop) { if ((this.attempt > this.options.attempt - 1) && this.state.error) { // 尝试次数过限 if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`) onFinish() return } if (this.state.rendered && this.state.loaded) return // 如果状态已经处于render或者加载完成状态 if (this._imageCache.has(this.src)) { // 如果加载完成 this.state.loaded = true this.render('loaded', true) this.state.rendered = true return onFinish() } this.renderLoading(() => { // 加载中 this.attempt++ // 尝试次数加1 this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options) this.record('loadStart') loadImageAsync({ src: this.src }, data => { // 图片加载完成 this.naturalHeight = data.naturalHeight this.naturalWidth = data.naturalWidth this.state.loaded = true this.state.error = false this.record('loadEnd') this.render('loaded', false) this.state.rendered = true this._imageCache.add(this.src) onFinish() }, err => { // 图片加载错误 !this.options.silent && console.error(err) this.state.error = true this.state.loaded = false this.render('error', false) }) }) }
loadImageAsync 图片异步加载方法,
const loadImageAsync = (item, resolve, reject) => { let image = new Image() // 初始化图片对象 if (!item || !item.src) { // 图片是否有地址 const err = new Error('image src is required') return reject(err) } image.src = item.src image.onload = function () { resolve({ naturalHeight: image.naturalHeight, naturalWidth: image.naturalWidth, src: image.src }) } image.onerror = function (e) { reject(e) }}
设置图片Render
/** * set element attribute with image'url and state * @param {object} lazyload listener object * @param {string} state will be rendered * @param {bool} inCache is rendered from cache * @return */ _elRenderer (listener, state, cache) { if (!listener.el) return const { el, bindType } = listener // 根据不同状态加载不同的图片资源 let src switch (state) { case 'loading': src = listener.loading break case 'error': src = listener.error break default: src = listener.src break } if (bindType) {// v-lazy: 后面的内容, 代表绑定的是这个属性 el.style[bindType] = 'url("' + src + '")'// 用于lazy load 背景图片 } else if (el.getAttribute('src') !== src) {// 普通lazyload image el.setAttribute('src', src) } el.setAttribute('lazy', state)// 自定义属性 lazy,用于给用于 根据此进行class搜索,设置指定状态的样式 this.$emit(state, listener, cache) // 触发当前状态的回调函数 this.options.adapter[state] && this.options.adapter[state](listener, this.options)// 触发adapter中的回调函数 if (this.options.dispatchEvent) { const event = new CustomEvent(state, { detail: listener }) el.dispatchEvent(event) } }
为新增节点
主要操作:找到对应的target(用于注册dom事件的dom节点;比如:页面滚动的dom节点),为其注册dom事件;为当前dom创建Listenr并添加到listener queue中。最后代用lazyLoadHandler()函数,加载图片
/* * add image listener to queue * @param {DOM} el * @param {object} binding vue directive binding * @param {vnode} vnode vue directive vnode * @return */ add (el, binding, vnode) { if (some(this.ListenerQueue, item => item.el === el)) { this.update(el, binding) return Vue.nextTick(this.lazyLoadHandler) } let { src, loading, error } = this._valueFormatter(binding.value) Vue.nextTick(() => { src = getBestSelectionFromSrcset(el, this.options.scale) || src this._observer && this._observer.observe(el) const container = Object.keys(binding.modifiers)[0] let $parent // 如果使用了container 修饰符, 那么查找我们定义的contianer; 如果没有使用当前dom所在最近的滚动parent
// 这个contianer是用于 设置监听dom事件的dom对象, 他的事件触发回调会触发图片的加载操作 if (container) { $parent = vnode.context.$refs[container] // if there is container passed in, try ref first, then fallback to getElementById to support the original usage $parent = $parent ? $parent.$el || $parent : document.getElementById(container) } if (!$parent) { $parent = scrollParent(el) }
// / 在当前dom绑定到vdom中, 为当前dom创建一个监听事件(此事件用于触发当前dom在不同时期的不同处理操作), 并将事件添加到事件队列里面
const newListener = new ReactiveListener({ bindType: binding.arg, $parent, el, loading, error, src, elRenderer: this._elRenderer.bind(this), options: this.options, imageCache: this._imageCache }) this.ListenerQueue.push(newListener) if (inBrowser) { this._addListenerTarget(window) // 增加目标对象,window this._addListenerTarget($parent)// 增加目标对象, 父级元素 } this.lazyLoadHandler() // 立即调用懒加载 Vue.nextTick(() => this.lazyLoadHandler()) // dom渲染后立即调用懒加载 }) }
检查元素是否在可是范围内
首先看y轴方向的判断:this.rect.top < window.innerHeight * this.options.preLoad, 是dom的顶部是否到了preload的位置;this.rect.bottom > this.options.preLoadTop 判断dom的底部是否到达了preload的位置
关于x轴方向就不做解析了,实现同y轴。
/* * check el is in view * @return {Boolean} el is in view */ checkInView () { this.getRect() return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) && (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0) }
三、简易版的懒加载
html
其中,img标签就是主角,data-src属性上面保存着我们后面需要动态加载的图片地址,初始化时图片没有设置任何链接
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
<img class="imgLazyLoad" data-src="http://office.qq.com/images/title.jpg" />
<ul>
<li>1</li>
<li>2</li>
<li>3</li>
<li>4</li>
<li>5</li>
</ul>
js
(function(){//立即执行函数
let imgList = [],delay,time = 250,offset = 0;
function _delay(){//函数防抖
clearTimeout(delay);
delay = setTimeout(() => {
_loadImg();
},time)
};
function _loadImg(){//执行图片加载
for(let i = 0 ; i < imgList.length; i++){
if(_isShow(imgList[i])){
imgList[i].src = imgList[i].getAttribute('data-src');
imgList.splice(i,1);
}
}
};
function _isShow(el){//判断img是否出现在可视窗口
let coords = el.getBoundingClientRect();
return (coords.left >= 0 && coords.left >= 0 && coords.top) <= (document.documentElement.clientHeight || window.innerHeight) + parseInt(offset);
};
function imgLoad(selector){//获取所有需要实现懒加载图片对象引用并设置window监听事件scroll
_selector = selector || '.imgLazyLoad';
let nodes = document.querySelectorAll(selector);
imgList = Array.apply(null,nodes);
window.addEventListener('scroll',_delay,false)
};
imgLoad('.imgLazyLoad')
})()
- imgList:保存所有图片节点的数组
- delay:保存的是setTimeout生成的引用
- time:控制防抖函数延迟执行的时间
- offset:设置图片距离可视区域多远则立即加载的偏差值
参考资料: