vue-lazyload 源碼解析

Lazy 類

/src/lazy.js

構(gòu)造函數(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 默認導(dǎo)出一個函數(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 具體做了什么

  1. 構(gòu)造函數(shù)初始化,配置參數(shù)定義 options 對象接收;

  2. 調(diào)用私有方法_initEvent,圖片加載狀態(tài)觸發(fā)機制,on 添加方法,once 添加僅執(zhí)行一次的方法,off 移除方法,emit 通知觸發(fā);

    // Event 結(jié)構(gòu)
    Event = {
        listeners: {
            loading: [],
            loaded: [],
            error: []
        }
    }
    
  3. 實例 ImageCache,提供一個圖片緩存容器;

  4. 設(shè)置元素進入 viewport 的監(jiān)聽模式,通過 listenEvents(scroll,wheel,touchmove等) 還是 通過 API IntersectionObserver,默認 listenEvents。遍歷 TargetQueue 數(shù)組內(nèi)所有元素,綁定觸發(fā)事件,以及方法 lazyLoadHandler;

  5. 暴露了 add 方法,對應(yīng) 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();
        })
    }
    
  1. 暴露了 update 方法,對應(yīng) Vue 自定義指令的update;

  2. 暴露了 lazyLoadHandler 方法,對應(yīng) Vue 自定義指令的componentUpdated——遍歷 ListenerQueue, 圖片元素進入 viewport 后,進行圖片加載;

  3. 暴露了 remove 方法,對應(yīng) Vue 自定義指令的unbind;

  4. 在入口 index.js 文件中 Vue.prototype.Lazyload = lazy*,可以在 vue 文件中,通過 ***this.Lazyload** 可以獲取到對應(yīng)屬性和方法。

ReactiveListener 類

/src/listener.js

構(gòu)造函數(shù)

定義變量接收實例化參數(shù)。

this.el = el
// 預(yù)期的圖片地址
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īng)根據(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回調(diào)函數(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

回過頭再來結(jié)合 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();
}

// 結(jié)合核心參數(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);
}

// 預(yù)期圖片加載
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');
        // 加載預(yù)期圖片
        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 標簽)遍歷后調(diào)用 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();
},
// 自定義指令,調(diào)用的是 ReactiveListener 實例的 load 方法
// 自定義組件,調(diào)用的 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 將設(shè)置背景圖的元素或者 img元素,通過 _addListenerTarget 方法收集與數(shù)組 TargetQueue 中,并遍歷觸發(fā)懶加載的方法,addEventListener 綁定在該元素上,觸發(fā)的事件為 lazyLoadHandler;

在需要懶加載的元素上設(shè)置屬性 data-src,這是期望的圖片地址(filter 配置項可以預(yù)先過濾賦值),元素上自定義 lazyLoad 表示圖片狀態(tài)(狀態(tài)變更后,adapter 中觸發(fā)回調(diào));

ListenerQueue 數(shù)組中收集的是 ReactiveListener 類的實例,主要是用于懶加載不同狀態(tài)下的圖片加載,loading - loaded - error;

當觸發(fā) EventListener 了,執(zhí)行 lazyLoadHandler 方法,根據(jù)算法,進入 viewport 后,ReactiveListener 元素如果與觸發(fā)元素匹配,則進行圖片的加載及渲染。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容