从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模块只负责状态的控制,根据状态的不一样执行不一样的业务逻辑。