Lazy 类
/src/lazy.js
构造函数
定义变量接收实例化参数。
this.version = '__VUE_LAZYLOAD_VERSION__'
this.mode = modeType.event
this.ListenerQueue = []
this.TargetIndex = 0
this.TargetQueue = []
this.options = {
// 不用打印debug信息
silent: silent,
// 是否绑定dom事件
dispatchEvent: !!dispatchEvent,
throttleWait: throttleWait || 200,
preLoad: preLoad || 1.3,
preLoadTop: preLoadTop || 0,
error: error || DEFAULT_URL,
loading: loading || DEFAULT_URL,
attempt: attempt || 3,
// 像素比(常见2或3)
scale: scale || getDPR(scale),
ListenEvents: listenEvents || DEFAULT_EVENTS,
hasbind: false,
supportWebp: supportWebp(),
// 过滤懒加载的监听器
filter: filter || {},
// 适配器,动态改变元素属性
adapter: adapter || {},
// 通过IntersectionObserver监听Viewport
observer: !!observer,
observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
}
lazy.js 默认导出一个函数,该函数返回一个 Lazy 类,形成闭包,保持对 Vue 的引用。
// lazy.js
export default function (Vue) {
return class Lazy {
constructor (options = {}) {
// init...
}
}
}
// index.js
import Lazy from './lazy.js';
export default {
install (Vue, options = {}) {
const LazyClass = lazy(Vue);
const lazy = new LazyClass(options);
}
}
判断是否支持Webp图片
const inBrowser = typeof window !== 'undefined' && window !== null;
function supportWebp() {
if (!inBrowser) return false;
let support = true;
try {
const elem = document.createElement('canvas');
if (!!(elem.getContext && elem.getContext('2d'))) {
support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
}
} catch (err) {
support = false;
}
return support;
}
Lazy 具体做了什么
构造函数初始化,配置参数定义 options 对象接收;
-
调用私有方法_initEvent,图片加载状态触发机制,once 添加仅执行一次的方法,emit 通知触发;
// Event 结构 Event = { listeners: { loading: [], loaded: [], error: [] } }
实例 ImageCache,提供一个图片缓存容器;
设置元素进入 viewport 的监听模式,通过 listenEvents(scroll,wheel,touchmove等) 还是 通过 API IntersectionObserver,默认 listenEvents。遍历 TargetQueue 数组内所有元素,绑定触发事件,以及方法 lazyLoadHandler;
-
暴露了 add 方法,对应 Vue 自定义指定的bind;
add (el, binding, vnode) { // 如果绑定的元素已在监听元素容器中,则触发更新,并加载image if (some(this.ListenerQueue, item => item.el === el)) { this.update(el, binding); return Vue.nextTick(this.lazyLoadHandler); } ... Vue.nextTick(() => { // 通过img标签的srcset属性以及像素比,选择适配屏幕的图片资源 src = getBestSelectionFromSrcset(el, this.options.scale) || src; // 如果存在通过 IntersectionObserver 监听 viewport,则绑定该元素 this._observer && this._observer.observe(el); // 定义父元素用于触发监听事件 // 方法1:自定义 /** * */ const container = Object.keys(binding.modifiers)[0]; let $parent if (container) { $parent = vnode.context.$refs[container]; $parent = $parent ? $parent.el || $parent : document.getElementById(container); } // 方法2:向上遍历,直至找到样式overflow为auto或scroll的祖先元素(不超过body、html) if (!$parent) { $parent = scrollParent(el); } // 收集监听者实例 const newListener = new ReactiveListener({ ... }); this.ListenerQueue.push(newListener); if (inBrowser) { // TargetQueue 收集需要触发 listenEvents 事件的元素 // 除了监听$parent还监听了window // 元素添加 el.addEventListener(ev, lazyLoadHandler) this._addListenerTarget(window); this._addListenerTarget($parent); } // 加载已满足 viewport 算法的图片 this.lazyLoadHandler(); }) }
暴露了 update 方法,对应 Vue 自定义指令的update;
暴露了 lazyLoadHandler 方法,对应 Vue 自定义指令的componentUpdated——遍历 ListenerQueue, 图片元素进入 viewport 后,进行图片加载;
暴露了 remove 方法,对应 Vue 自定义指令的unbind;
在入口 index.js 文件中 Vue.prototype.Lazyload** 可以获取到对应属性和方法。
ReactiveListener 类
/src/listener.js
构造函数
定义变量接收实例化参数。
this.el = el
// 预期的图片地址
this.src = src
// 加载异常的图片
this.error = error
// 加载中的图片
this.loading = loading
// v-lazy:bindType v-lazy:background-image
this.bindType = bindType
this.attempt = 0
this.cors = cors
this.naturalHeight = 0
this.naturalWidth = 0
this.options = options
this.rect = null
this.$parent = $parent
this.elRenderer = elRenderer
this._imageCache = imageCache
this.performanceData = {
init: Date.now(),
loadStart: 0,
loadEnd: 0
}
filter 方法将配置的 filter 对象中的方法执行,接收两个参数,一个为 ReactiveListener 实例,一个为 options 参数对象。
filter () {
ObjectKeys(this.options.filter).map(key => {
this.options.filter[key](this, this.options);
})
}
initState 方法给元素添加 data-set 属性,值为图片地址 src,并且定义了图片状态对象 state 。在 Lazy 中已经根据像素比选择了最适配屏幕的图片,顾这里不需要考虑 srcset 属性。另外,我们自定义指令是 v-lazy,到目前为止,还没有给图片的 src 属性赋值。
this.state = {
loading: false,
error: false,
loaded: false,
rendered: false
}
render 方法,是在 Lazy 中实例化 ReactiveListener 时传递过来的参数。
// lazy.js
const newListener = new ReactiveListener({
...
elRenderer: this._elRender.bind(this),
...
})
_elRenderer (listener, state, cache) {
if (!listener.el) return;
const { el, bindType } = listen;
let src
switch (state) {
case 'loading':
src = listener.loading;
break;
case 'error':
src = listener.error;
break;
case 'loaded':
default:
src = listener.src;
break;
}
if (bindType) {
// 背景图片
el.style[bindType] = `url("${src}")`;
} else if (el.getAttribute('src') !== src) {
// 在这里正式给图片 src 属性赋值(加载中,加载完成,加载异常)
el.setAttribute('src', src);
}
// 标签自定义 lazy 属性,标记图片加载状态
el.setAttribute('lazy', state);
// 通过 $on 方法订阅的通知
// 一般在单个 Vue 文件中通过 this.$Lazyload.$on(name, fn),fn回调函数接收参数为 ReactiveListener 实例
this.$emit(state, listener, cache);
// adapter 适配器参数一般在 main.js 文件中配置,在图片加载的状态变更时触发,生命周期
this.options.adapter[state] && this.options.adapter[state](listener, this.options);
}
// listener.js
this.render('loading', state);
render (state, cache) {
this.elRenderer(this, state, cache);
}
lazyLoadHandler
回过头再来结合 lazy.js 中的 lazyLoadHandler 方法与 ReactiveListener 暴露的方法来看。
// lazy.js
_lazyLoadHandler () {
const freeList = [];
// ListenerQueue 中是 ReactiveListener 的实例
this.ListenerQueue.forEach(listener => {
if (!listener.el || !listener.el.parentNode) {
freeList.push(listen);
}
const catIn = listener.checkInView();
if (!catIn) = return;
listener.load();
})
freeList.forEach(item => {
remove(this.ListenerQueue, item);
item.$destroy();
})
}
// listener.js
getRect () {
this.rect = this.el.getBoundingClientRect();
}
// 结合核心参数 preLoad 算出何时加载
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);
}
// 预期图片加载
load (onFinish = () => {}) {
// 超出尝试次数且加载失败
if (this.attempt > this.options.attempt - 1 && this.state.error) {
return onFinish();
}
if (this.state.rendered && this.state.loaded) return;
//是否有缓存
if (this._imageCache.has(this.src)) {
this.state.loaded = true;
this.render('loaded', true);
this.state.rendered = true;
return onFinish();
}
// 加载 loading 的图片
this.renderLoading(() => {
this.attempt++;
// 生命周期 beforeLoad
this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options);
// 记录图片开始加载的时间
this.record('loadStart');
// 加载预期图片
loadImageAsync({
src: this.src,
cors: this.cors
}, data => {
// resolve
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 => {
// reject
!this.options.silent && console.error(err);
this.state.error = true;
this.state.loaded = false;
this.render('error', false);
})
})
}
LazyContainer 类
/src/lazy-container.js
LazyContainer 的核心是 container 下的选择器selector(默认 img 标签)遍历后调用 lazy 的 add 方法进行绑定,自定义指令 v-lazyload-container。
const imgs = this.getImgs();
imgs.forEach(el => {
this.lazy.add(el, assign({}, this.binding, {
value: {
src: 'dataset' in el ? el.dataset.src : el.getAttribute('data-src'),
error: ('dataset' in el ? el.dataset.error : el.getAttribute('data-error')) || this.options.error,
loading: ('dataset' in el ? el.dataset.loading : el.getAttribute('data-loading')) || this.options.loading
}
}), this.vnode)
})
LazyComponent 类
/src/lazy-component.js
上述实现元素绑定主要是通过自定义指令 v-lazy,v-lazy-container。那么 LazyComponent 则是通过注册的 lazy-component 组件,完成绑定,默认渲染成为 div 标签,作为 img 的容器。
// lazy-component.js
mounted () {
this.el = this.$el;
lazy.addLazyBox(this);
lazy.lazyLoadHandler();
},
// 自定义指令,调用的是 ReactiveListener 实例的 load 方法
// 自定义组件,调用的 methods 中的 load 方法
// 如果需要更高的定制化,推荐使用自定义指令
methods: {
getRect () {},
checkInView () {},
load () {}
}
// lazy.js
addLazyBox (vm) {
// 添加至图片加载实例容器
this.listenerQueue.push(vm);
if (inBrowser) {
// 元素添加 el.addEventListener(ev, lazyLoadHandler)
this._addListenerTarget(window);
this._observer && this._observer.observe(vm.el);
if (vm.$el && vm.$em.parentNode) {
this._addListenerTarget(vm.$el.parentNode);
}
}
}
LazyImage 类
/src/lazy-image.js
通 LazyComponent 组件,只不过 LazyImage 注册的 lazy-image 组件,渲染成的是 img 标签,多了 src 属性。
核心过程
通过自定义指令 v-lazy 将设置背景图的元素或者 img元素,通过 _addListenerTarget 方法收集与数组 TargetQueue 中,并遍历触发懒加载的方法,addEventListener 绑定在该元素上,触发的事件为 lazyLoadHandler;
在需要懒加载的元素上设置属性 data-src,这是期望的图片地址(filter 配置项可以预先过滤赋值),元素上自定义 lazyLoad 表示图片状态(状态变更后,adapter 中触发回调);
ListenerQueue 数组中收集的是 ReactiveListener 类的实例,主要是用于懒加载不同状态下的图片加载,loading - loaded - error;
当触发 EventListener 了,执行 lazyLoadHandler 方法,根据算法,进入 viewport 后,ReactiveListener 元素如果与触发元素匹配,则进行图片的加载及渲染。