vue-lazyload 源碼解析

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 具體做了什么

  1. 構造函數(shù)初始化恩尾,配置參數(shù)定義 options 對象接收弛说;

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

    // Event 結構
    Event = {
        listeners: {
            loading: [],
            loaded: [],
            error: []
        }
    }
    
  3. 實例 ImageCache,提供一個圖片緩存容器进鸠;

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

  5. 暴露了 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();
        })
    }
    
  1. 暴露了 update 方法枫弟,對應 Vue 自定義指令的update邢享;

  2. 暴露了 lazyLoadHandler 方法鹏往,對應 Vue 自定義指令的componentUpdated——遍歷 ListenerQueue淡诗, 圖片元素進入 viewport 后骇塘,進行圖片加載;

  3. 暴露了 remove 方法韩容,對應 Vue 自定義指令的unbind;

  4. 在入口 index.js 文件中 Vue.prototype.Lazyload = lazy*款违,可以在 vue 文件中,通過 ***this.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-lazyv-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ā)元素匹配丐黄,則進行圖片的加載及渲染斋配。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市灌闺,隨后出現(xiàn)的幾起案子艰争,更是在濱河造成了極大的恐慌,老刑警劉巖桂对,帶你破解...
    沈念sama閱讀 216,470評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件甩卓,死亡現(xiàn)場離奇詭異,居然都是意外死亡蕉斜,警方通過查閱死者的電腦和手機逾柿,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,393評論 3 392
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來宅此,“玉大人机错,你說我怎么就攤上這事「竿螅” “怎么了弱匪?”我有些...
    開封第一講書人閱讀 162,577評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長璧亮。 經常有香客問我萧诫,道長斥难,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,176評論 1 292
  • 正文 為了忘掉前任财搁,我火速辦了婚禮蘸炸,結果婚禮上躬络,老公的妹妹穿的比我還像新娘尖奔。我一直安慰自己,他們只是感情好穷当,可當我...
    茶點故事閱讀 67,189評論 6 388
  • 文/花漫 我一把揭開白布提茁。 她就那樣靜靜地躺著,像睡著了一般馁菜。 火紅的嫁衣襯著肌膚如雪茴扁。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,155評論 1 299
  • 那天汪疮,我揣著相機與錄音峭火,去河邊找鬼。 笑死智嚷,一個胖子當著我的面吹牛卖丸,可吹牛的內容都是我干的。 我是一名探鬼主播盏道,決...
    沈念sama閱讀 40,041評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼稍浆,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了猜嘱?” 一聲冷哼從身側響起衅枫,我...
    開封第一講書人閱讀 38,903評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎朗伶,沒想到半個月后弦撩,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經...
    沈念sama閱讀 45,319評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡论皆,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,539評論 2 332
  • 正文 我和宋清朗相戀三年孤钦,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片纯丸。...
    茶點故事閱讀 39,703評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡偏形,死狀恐怖,靈堂內的尸體忽然破棺而出觉鼻,到底是詐尸還是另有隱情俊扭,我是刑警寧澤,帶...
    沈念sama閱讀 35,417評論 5 343
  • 正文 年R本政府宣布坠陈,位于F島的核電站萨惑,受9級特大地震影響捐康,放射性物質發(fā)生泄漏。R本人自食惡果不足惜庸蔼,卻給世界環(huán)境...
    茶點故事閱讀 41,013評論 3 325
  • 文/蒙蒙 一解总、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧姐仅,春花似錦花枫、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,664評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至馒疹,卻和暖如春佳簸,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背颖变。 一陣腳步聲響...
    開封第一講書人閱讀 32,818評論 1 269
  • 我被黑心中介騙來泰國打工生均, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人腥刹。 一個月前我還...
    沈念sama閱讀 47,711評論 2 368
  • 正文 我出身青樓马胧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親肛走。 傳聞我的和親對象是個殘疾皇子漓雅,可洞房花燭夜當晚...
    茶點故事閱讀 44,601評論 2 353

推薦閱讀更多精彩內容