Lazy 類
/src/lazy.js
構造函數(shù)
定義變量接收實例化參數(shù)戴甩。
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(),
// 過濾懶加載的監(jiān)聽器
filter: filter || {},
// 適配器纪隙,動態(tài)改變元素屬性
adapter: adapter || {},
// 通過IntersectionObserver監(jiān)聽Viewport
observer: !!observer,
observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
}
lazy.js 默認導出一個函數(shù),該函數(shù)返回一個 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 具體做了什么
構造函數(shù)初始化恩尾,配置參數(shù)定義 options 對象接收弛说;
-
調用私有方法_initEvent,圖片加載狀態(tài)觸發(fā)機制翰意,once 添加僅執(zhí)行一次的方法,emit 通知觸發(fā)醒第;
// Event 結構 Event = { listeners: { loading: [], loaded: [], error: [] } }
實例 ImageCache,提供一個圖片緩存容器进鸠;
設置元素進入 viewport 的監(jiān)聽模式稠曼,通過 listenEvents(scroll,wheel,touchmove等) 還是 通過 API IntersectionObserver,默認 listenEvents客年。遍歷 TargetQueue 數(shù)組內所有元素霞幅,綁定觸發(fā)事件,以及方法 lazyLoadHandler量瓜;
-
暴露了 add 方法司恳,對應 Vue 自定義指定的bind;
add (el, binding, vnode) { // 如果綁定的元素已在監(jiān)聽元素容器中绍傲,則觸發(fā)更新扔傅,并加載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 監(jiān)聽 viewport烫饼,則綁定該元素 this._observer && this._observer.observe(el); // 定義父元素用于觸發(fā)監(jiān)聽事件 // 方法1:自定義 /** * <div ref="containerName"><img v-lazy.containerName="imgUrl" /></div> */ 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); } // 收集監(jiān)聽者實例 const newListener = new ReactiveListener({ ... }); this.ListenerQueue.push(newListener); if (inBrowser) { // TargetQueue 收集需要觸發(fā) listenEvents 事件的元素 // 除了監(jiān)聽$parent還監(jiān)聽了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
構造函數(shù)
定義變量接收實例化參數(shù)插爹。
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 對象中的方法執(zhí)行,接收兩個參數(shù)请梢,一個為 ReactiveListener 實例赠尾,一個為 options 參數(shù)對象。
filter () {
ObjectKeys(this.options.filter).map(key => {
this.options.filter[key](this, this.options);
})
}
initState 方法給元素添加 data-set 屬性毅弧,值為圖片地址 src气嫁,并且定義了圖片狀態(tài)對象 state 。在 Lazy 中已經根據(jù)像素比選擇了最適配屏幕的圖片够坐,顧這里不需要考慮 srcset 屬性寸宵。另外,我們自定義指令是 v-lazy元咙,到目前為止梯影,還沒有給圖片的 src 屬性賦值。
this.state = {
loading: false,
error: false,
loaded: false,
rendered: false
}
render 方法庶香,是在 Lazy 中實例化 ReactiveListener 時傳遞過來的參數(shù)甲棍。
// 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 屬性救军,標記圖片加載狀態(tài)
el.setAttribute('lazy', state);
// 通過 $on 方法訂閱的通知
// 一般在單個 Vue 文件中通過 this.$Lazyload.$on(name, fn),fn回調函數(shù)接收參數(shù)為 ReactiveListener 實例
this.$emit(state, listener, cache);
// adapter 適配器參數(shù)一般在 main.js 文件中配置倘零,在圖片加載的狀態(tài)變更時觸發(fā)唱遭,生命周期
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();
}
// 結合核心參數(shù) 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 = () => {}) {
// 超出嘗試次數(shù)且加載失敗
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
上述實現(xiàn)元素綁定主要是通過自定義指令 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 方法收集與數(shù)組 TargetQueue 中恨搓,并遍歷觸發(fā)懶加載的方法院促,addEventListener 綁定在該元素上,觸發(fā)的事件為 lazyLoadHandler斧抱;
在需要懶加載的元素上設置屬性 data-src常拓,這是期望的圖片地址(filter 配置項可以預先過濾賦值),元素上自定義 lazyLoad 表示圖片狀態(tài)(狀態(tài)變更后辉浦,adapter 中觸發(fā)回調)墩邀;
ListenerQueue 數(shù)組中收集的是 ReactiveListener 類的實例,主要是用于懶加載不同狀態(tài)下的圖片加載盏浙,loading - loaded - error眉睹;
當觸發(fā) EventListener 了,執(zhí)行 lazyLoadHandler 方法废膘,根據(jù)算法竹海,進入 viewport 后,ReactiveListener 元素如果與觸發(fā)元素匹配丐黄,則進行圖片的加載及渲染斋配。