從package.json中的script命令腳本瞭解項目構建的配置文件是build.js。vue-lazyload庫是經過rollup構建的,其中的input屬性值src/index.js
爲源碼入口。vue
// build.js async function build () { try { const bundle = await rollup.rollup({ input: path.resolve(__dirname, 'src/index.js'), plugins: [ resolve(), commonjs(), babel({ runtimeHelpers: true }), uglify() ] }) let { code } = await bundle.generate({ format: 'umd', name: 'VueLazyload' }) code = rewriteVersion(code) await write(path.resolve(__dirname, 'vue-lazyload.js'), code) console.log('Vue-Lazyload.js v' + version + ' builded') } catch (e) { console.log(e) } } build()
// src/index.js export default { /* * install function * @param {Vue} Vue * @param {object} options lazyload options */ install (Vue, options = {}) { const LazyClass = Lazy(Vue) const lazy = new LazyClass(options) const lazyContainer = new LazyContainer({ lazy }) const isVue2 = Vue.version.split('.')[0] === '2' Vue.prototype.$Lazyload = lazy if (options.lazyComponent) { Vue.component('lazy-component', LazyComponent(lazy)) } if (options.lazyImage) { Vue.component('lazy-image', LazyImage(lazy)) } if (isVue2) { Vue.directive('lazy', { bind: lazy.add.bind(lazy), update: lazy.update.bind(lazy), componentUpdated: lazy.lazyLoadHandler.bind(lazy), unbind: lazy.remove.bind(lazy) }) Vue.directive('lazy-container', { bind: lazyContainer.bind.bind(lazyContainer), componentUpdated: lazyContainer.update.bind(lazyContainer), unbind: lazyContainer.unbind.bind(lazyContainer) }) } else { Vue.directive('lazy', { bind: lazy.lazyLoadHandler.bind(lazy), update (newValue, oldValue) { assign(this.vm.$refs, this.vm.$els) lazy.add(this.el, { modifiers: this.modifiers || {}, arg: this.arg, value: newValue, oldValue: oldValue }, { context: this.vm }) }, unbind () { lazy.remove(this.el) } }) Vue.directive('lazy-container', { update (newValue, oldValue) { lazyContainer.update(this.el, { modifiers: this.modifiers || {}, arg: this.arg, value: newValue, oldValue: oldValue }, { context: this.vm }) }, unbind () { lazyContainer.unbind(this.el) } }) } } }
src/index.js
中主要作了兩件事:node
這裏lazy指令跟lazyContainer指令是兩種不一樣的用法,從vue-lazyload文檔裏能夠查看其中的區別。此次主要經過lazy指令來對vue-lazyload進行分析。json
// src/lazy.js return class Lazy { constructor ({ preLoad, error, throttleWait, preLoadTop, dispatchEvent, loading, attempt, silent = true, scale, listenEvents, hasbind, filter, adapter, observer, observerOptions }) { this.version = '__VUE_LAZYLOAD_VERSION__' this.mode = modeType.event this.ListenerQueue = [] this.TargetIndex = 0 this.TargetQueue = [] this.options = { silent: silent, dispatchEvent: !!dispatchEvent, throttleWait: throttleWait || 200, preLoad: preLoad || 1.3, preLoadTop: preLoadTop || 0, error: error || DEFAULT_URL, loading: loading || DEFAULT_URL, attempt: attempt || 3, scale: scale || getDPR(scale), ListenEvents: listenEvents || DEFAULT_EVENTS, hasbind: false, supportWebp: supportWebp(), filter: filter || {}, adapter: adapter || {}, observer: !!observer, observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS } this._initEvent() this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait) this.setMode(this.options.observer ? modeType.observer : modeType.event) } // ... }
Lazy類的構造函數中定義了一系列屬性,這些屬性一部分是內部私有屬性,一部分在vue-lazyload文檔中有介紹,這裏就不過多闡述了。主要了解一下構造函數中執行的三行代碼:緩存
// src/lazy.js // 第一行 this._initEvent() // 第二行 this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait) // 第三行 this.setMode(this.options.observer ? modeType.observer : modeType.event)
第一行對loading、loaded、error事件監聽方法的初始化:babel
// src/lazy.js _initEvent () { this.Event = { listeners: { loading: [], loaded: [], error: [] } } this.$on = (event, func) => { if (!this.Event.listeners[event]) this.Event.listeners[event] = [] this.Event.listeners[event].push(func) } this.$once = (event, func) => { const vm = this function on () { vm.$off(event, on) func.apply(vm, arguments) } this.$on(event, on) } this.$off = (event, func) => { if (!func) { if (!this.Event.listeners[event]) return this.Event.listeners[event].length = 0 return } remove(this.Event.listeners[event], func) } this.$emit = (event, context, inCache) => { if (!this.Event.listeners[event]) return this.Event.listeners[event].forEach(func => func(context, inCache)) } }
第二行代碼對懶加載處理函數進行了節流處理,這裏咱們須要關心的地方有懶加載處理函數、節流處理函數app
// src/lazy.js // 對懶加載處理函數進行節流處理 this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait) // 懶加載處理函數 // 將監聽隊列中loaded狀態的監聽對象取出存放在freeList中並刪掉,判斷未加載的監聽對象是否處在預加載位置,若是是則執行load方法。 _lazyLoadHandler () { const freeList = [] this.ListenerQueue.forEach((listener, index) => { if (!listener.state.error && listener.state.loaded) { return freeList.push(listener) } // 判斷當前監聽對象是否在預加載位置,若是是則執行load方法開始加載 const catIn = listener.checkInView() if (!catIn) return listener.load() }) freeList.forEach(vm => remove(this.ListenerQueue, vm)) } // src/util.js // 函數節流封裝函數 // 接收兩個參數,action爲待執行的行爲操做,delay爲節流延遲時間 function throttle (action, delay) { let timeout = null let lastRun = 0 return function () { if (timeout) { return } let elapsed = Date.now() - lastRun let context = this let args = arguments let runCallback = function () { lastRun = Date.now() timeout = false action.apply(context, args) } if (elapsed >= delay) { runCallback() } else { timeout = setTimeout(runCallback, delay) } } }
第三行設置監聽模式,咱們一般使用scroll
或者IntersectionObserver
來判斷,元素是否進入視圖,若進入視圖則需爲圖片加載真實路徑。若是使用scroll
則mode
值爲event
,若是使用IntersectionObserver
則mode
值爲observer
;dom
// src/lazy.js this.setMode(this.options.observer ? modeType.observer : modeType.event) setMode (mode) { if (!hasIntersectionObserver && mode === modeType.observer) { mode = modeType.event } this.mode = mode // event or observer if (mode === modeType.event) { if (this._observer) { this.ListenerQueue.forEach(listener => { this._observer.unobserve(listener.el) }) this._observer = null } this.TargetQueue.forEach(target => { this._initListen(target.el, true) }) } else { this.TargetQueue.forEach(target => { this._initListen(target.el, false) }) this._initIntersectionObserver() } }
// src/index.js Vue.directive('lazy', { bind: lazy.add.bind(lazy), update: lazy.update.bind(lazy), componentUpdated: lazy.lazyLoadHandler.bind(lazy), unbind: lazy.remove.bind(lazy) })
首先咱們來了解一下lazy指令中聲明的幾個鉤子函數異步
當指令第一次綁定到元素上時,調用的是lazy.add方法:async
// src/lazy.js add (el, binding, vnode) { // 判斷當前元素是否在監聽隊列中,若是在則執行update方法。並在下次dom更新循環結束以後延遲迴調懶加載方法lazyLoadHandler if (some(this.ListenerQueue, item => item.el === el)) { this.update(el, binding) return Vue.nextTick(this.lazyLoadHandler) } // 獲取圖片真實路徑,loading狀態佔位圖路徑,加載失敗佔位圖路徑 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 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) } const newListener = new ReactiveListener({ bindType: binding.arg, $parent, el, loading, error, src, elRenderer: this._elRenderer.bind(this), options: this.options }) this.ListenerQueue.push(newListener) if (inBrowser) { this._addListenerTarget(window) this._addListenerTarget($parent) } this.lazyLoadHandler() Vue.nextTick(() => this.lazyLoadHandler()) }) }
lazy.add
方法中的主要邏輯就兩點:函數
ListenerQueue
中,則直接調用this.update
方法並再dom渲染完畢以後執行懶加載處理函數this.lazyLoadHandler
newListener
並將其存放在監聽隊列ListenerQueue
中。window
或$parent
爲scroll事件的監聽目標對象。this.lazyLoadHandler()
。由於lazy指令的update鉤子函數調用的即是lazy的update方法,因此第一點咱們放在後面再講。第二點中咱們主要目標是瞭解這個newListener
對象。
// src/listener.js export default class ReactiveListener { constructor ({ el, src, error, loading, bindType, $parent, options, elRenderer }) { this.el = el this.src = src this.error = error this.loading = loading this.bindType = bindType this.attempt = 0 this.naturalHeight = 0 this.naturalWidth = 0 this.options = options this.rect = null this.$parent = $parent this.elRenderer = elRenderer this.performanceData = { init: Date.now(), loadStart: 0, loadEnd: 0 } this.filter() this.initState() this.render('loading', false) } // ... }
在ReactiveListener類的構造函數末尾執行了三個方法:
data-src
屬性上,併爲監聽對象添加error,loaded,rendered狀態。// src/listener.js initState () { if ('dataset' in this.el) { this.el.dataset.src = this.src } else { this.el.setAttribute('data-src', this.src) } this.state = { error: false, loaded: false, rendered: false } }
_elRenderer
方法。
loading
設置當前圖片的路徑爲loading狀態佔位圖路徑。this.$emit(state, listener, cache)
// src/listener.js render (state, cache) { this.elRenderer(this, state, cache) } // src/lazy.js _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) { el.style[bindType] = 'url("' + src + '")' } else if (el.getAttribute('src') !== src) { el.setAttribute('src', src) } el.setAttribute('lazy', state) this.$emit(state, listener, cache) this.options.adapter[state] && this.options.adapter[state](listener, this.options) if (this.options.dispatchEvent) { const event = new CustomEvent(state, { detail: listener }) el.dispatchEvent(event) } }
到這一步咱們將lazy指令綁定的全部dom元素封裝成一個個ReactiveListener監聽對象,並將其存放在ListenerQueue隊列中,當前元素顯示的是loading狀態的佔位圖,dom渲染完畢後將會執行懶加載處理函數_lazyLoadHandler
。再來看一下該函數代碼:
// src/lazy.js _lazyLoadHandler () { const freeList = [] this.ListenerQueue.forEach((listener, index) => { if (!listener.state.error && listener.state.loaded) { return freeList.push(listener) } const catIn = listener.checkInView() if (!catIn) return listener.load() }) freeList.forEach(vm => remove(this.ListenerQueue, vm)) }
懶加載函數乾的事情就兩點:
第一點邏輯一目瞭然,不須要再過多闡述。咱們主要了解一下_lazyLoadHandler
中使用到的兩個方法。一是判斷當前對象是否處在預加載位置的listener.checkInView()
;另外一個是監聽對象的load方法:listener.load()
;
checkInView方法內部實現:判斷元素位置是否處在預加載視圖內,若元素處在視圖內部則返回true,反之則返回false。
// src/listener.js 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) } getRect () { this.rect = this.el.getBoundingClientRect() }
// src/listener.js load (onFinish = noop) { // 若嘗試次數完畢而且對象狀態爲error,則打印錯誤提示並結束。 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 } // 若當前對象狀態爲loaded而且路徑已緩存在imageCache中,則調用this.render('loaded', true)渲染dom真實路徑。 if (this.state.loaded || imageCache[this.src]) { this.state.loaded = true onFinish() return this.render('loaded', true) } // 若以上條件都不成立,則調用renderLoading方法渲染loading狀態的圖片。 this.renderLoading(() => { this.attempt++ 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) imageCache[this.src] = 1 onFinish() }, err => { !this.options.silent && console.error(err) this.state.error = true this.state.loaded = false this.render('error', false) }) }) } // renderLoading方法 renderLoading (cb) { // 異步加載圖片 loadImageAsync( { src: this.loading }, data => { this.render('loading', false) cb() }, () => { // handler `loading image` load failed cb() if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`) } ) } // loadImageAsync方法 const loadImageAsync = (item, resolve, reject) => { let image = new Image() image.src = item.src image.onload = function () { resolve({ naturalHeight: image.naturalHeight, naturalWidth: image.naturalWidth, src: image.src }) } image.onerror = function (e) { reject(e) } }
整個調用順序爲:
到這一步全部處於預加載容器視圖內的元素加載真實路徑完畢。
分析完bind鉤子,咱們再來看lazy指令上聲明的update鉤子函數:update: lazy.update.bind(lazy)
;update鉤子上綁定的是lazy的update方法,進入lazy.update方法:
// src/index.js update (el, binding, vnode) { let { src, loading, error } = this._valueFormatter(binding.value) src = getBestSelectionFromSrcset(el, this.options.scale) || src const exist = find(this.ListenerQueue, item => item.el === el) if (!exist) { this.add(el, binding, vnode) } else { exist.update({ src, loading, error }) } if (this._observer) { this._observer.unobserve(el) this._observer.observe(el) } this.lazyLoadHandler() Vue.nextTick(() => this.lazyLoadHandler()) }
update方法裏首先判斷當前元素是否存在監聽隊列ListenerQueue中,若不存在則執行this.add(el, binding, vnode)
;add方法在分析bind鉤子時候已經講過,這裏可參考上文。若存在,則調用監聽對象上的update方法: exist.update
,執行完後調用懶加載處理函數this.lazyLoadHandler()
;
// src/listener.js update ({ src, loading, error }) { // 取出以前圖片的真實路徑 const oldSrc = this.src // 將新的圖片路徑設置爲監聽對象的真實路徑 this.src = src this.loading = loading this.error = error this.filter() // 比較兩個路徑是否相等,若不相等,則初始化加載次數以及初始化對象狀態。 if (oldSrc !== this.src) { this.attempt = 0 this.initState() } }
分析完lazy指令的bind,update鉤子,咱們瞭解到了圖片預加載邏輯以下:
在初始化階段以及圖片路徑發生變化階段的預加載邏輯咱們已經整明白了。最後咱們來看一下在容器發生滾動產生的圖片預加載動做的整個邏輯。
在以前的代碼裏就添加過目標容器,咱們來重溫一下這段代碼:
// src/lazy.js setMode (mode) { if (!hasIntersectionObserver && mode === modeType.observer) { mode = modeType.event } this.mode = mode // event or observer if (mode === modeType.event) { if (this._observer) { this.ListenerQueue.forEach(listener => { this._observer.unobserve(listener.el) }) this._observer = null } this.TargetQueue.forEach(target => { this._initListen(target.el, true) }) } else { this.TargetQueue.forEach(target => { this._initListen(target.el, false) }) this._initIntersectionObserver() } }
若是使用scroll形式,則調用this._initListen(target.el, true)
這段代碼爲目標容器添加事件監聽。默認監聽'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
這些事件,當事件觸發時調用預加載處理函數lazyLoadHandler
// src/lazy.js const DEFAULT_EVENTS = ['scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'] // this.options.ListenEvents : listenEvents || DEFAULT_EVENTS, _initListen (el, start) { this.options.ListenEvents.forEach( (evt) => _[start ? 'on' : 'off'](el, evt, this.lazyLoadHandler) ) } // src/util.js const _ = { on (el, type, func, capture = false) { if (supportsPassive) { el.addEventListener(type, func, { capture: capture, passive: true }) } else { el.addEventListener(type, func, capture) } }, off (el, type, func, capture = false) { el.removeEventListener(type, func, capture) } }
對IntersectionObserver的使用你們能夠在網上查詢相關文檔。它能夠用來監聽元素是否進入了設備的可視區域以內,而不須要頻繁的計算來作這個判斷。
當使用IntersectionObserver模式時,主要作兩步處理:
// src/lazy.js _initIntersectionObserver () { if (!hasIntersectionObserver) return this._observer = new IntersectionObserver( this._observerHandler.bind(this), this.options.observerOptions ) if (this.ListenerQueue.length) { this.ListenerQueue.forEach( listener => { this._observer.observe(listener.el) } ) } } _observerHandler (entries, observer) { entries.forEach(entry => { if (entry.isIntersecting) { this.ListenerQueue.forEach(listener => { if (listener.el === entry.target) { if (listener.state.loaded) return this._observer.unobserve(listener.el) listener.load() } }) } }) }
當使用scroll模式時,圖片預加載邏輯:
'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
事件觸發,調用懶加載處理函數lazyloadHandle
ListenerQueue
,刪除狀態爲loaded的監聽對象ListenerQueue
,判斷該監聽對象是否存在預加載視圖容器中,若存在,則調用load方法異步加載真實路徑。當使用IntersectionObserver模式時,圖片預加載邏輯
'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
的綁定經過對vue-lazyload的源碼分析,咱們明白了lazyload的實現原理,也瞭解到了做者代碼結構的設計方式。源碼中lazy模塊和listener模塊的業務職責分工明確。lazy模塊負責dom相關的處理,如爲dom元素建立listener,爲容器target綁定dom事件,dom元素的渲染等。listener模塊只負責狀態的控制,根據狀態的不一樣執行不一樣的業務邏輯。