懒就是生产力之图片懒加载
最近负责的项目渐渐的由业务型转向营销型,营销内容越来越多,图片也就多了起来。图片一多起来问题就来了,一上来几十张图片加载起来半天都过去了,咋办?凉拌--懒加载
什么是懒加载
懒加载也叫延迟加载,本质上就是按需加载,即只有当图片dom已经在或者即将进入用户视线范围内的时候才去加载对应的dom图片。前两年流行的瀑布流其实就是按需加载的一个应用实例。
在懒加载的模型中,一个页面可以氛围三个区域如图,视图区、即将进入视图区、视图远区(即图中的不加载区)。
当一个图片在视图区或者随着屏幕滚动即将进入视图区的时候,就开始加载图片。
懒加载的处理流程
大象装冰箱,也得分三步,懒加载也一样
首先,将图片标签加入懒加载队列lazyQueue,并且给图片加载一个默认图,一般是个品牌logo或者1px*1px的空白图片做拉伸。加载默认图主要是为了看起来不那么尴尬,用户普遍尴尬耐受力强的可以不加~
其次,添加屏幕滚动监听事件,如mousemove、wheel滚动等,实时监听懒加载队列lazyQueue中的dom位置变动。这一步算是懒加载的核心了吧。
最后,在监听器中实时判断图片dom的位置,如果已进入加载区的位置就去加载图片,加载完成后把响应的dom移出lazyQueue。
直接上代码吧,怎么感觉这么墨迹。。。
完整代码
/* global Image */
if (!Array.prototype.$remove) {
Array.prototype.$remove = function(item) {
if (!this.length) return
const index = this.indexOf(item)
if (index > -1) {
return this.splice(index, 1)
}
}
}
export default (Vue, Options = {}) => {
const isVueNext = Vue.version.split('.')[0] === '2'
const DEFAULT_URL = Options.bgImgUrl || 'https//img.aiyoumi.com/null/2018423/101212916/20180423101212_1x1.png?height=1&width=1'
const Init = {
preLoad: Options.preLoad || 1.8,
error: Options.error || DEFAULT_URL,
loading: Options.loading || DEFAULT_URL,
attempt: Options.attempt || 3,
scale: Options.scale || window.devicePixelRatio,
hasbind: false
}
const Listeners = []
const imageCache = []
const throttle = function(action, delay) {
let timeout = null
let lastRun = 0
return function() {
if (timeout) {
return
}
let elapsed = (+new Date()) - lastRun
let context = this
let args = arguments
let runCallback = function() {
lastRun = +new Date()
timeout = false
action.apply(context, args)
}
if (elapsed >= delay) {
runCallback()
} else {
timeout = setTimeout(runCallback, delay)
}
}
}
const _ = {
on(el, type, func) {
el.addEventListener(type, func)
},
off(el, type, func) {
el.removeEventListener(type, func)
}
}
const lazyLoadHandler = throttle(() => {
for (let i = 0, len = Listeners.length; i < len; ++i) {
checkCanShow(Listeners[i])
}
}, 300)
const onListen = (el, start) => {
if (start) {
_.on(el, 'scroll', lazyLoadHandler)
_.on(el, 'wheel', lazyLoadHandler)
_.on(el, 'mousewheel', lazyLoadHandler)
_.on(el, 'resize', lazyLoadHandler)
_.on(el, 'animationend', lazyLoadHandler)
_.on(el, 'transitionend', lazyLoadHandler)
} else {
Init.hasbind = false
_.off(el, 'scroll', lazyLoadHandler)
_.off(el, 'wheel', lazyLoadHandler)
_.off(el, 'mousewheel', lazyLoadHandler)
_.off(el, 'resize', lazyLoadHandler)
_.off(el, 'animationend', lazyLoadHandler)
_.off(el, 'transitionend', lazyLoadHandler)
}
}
const checkCanShow = (listener) => {
if (imageCache.indexOf(listener.src) > -1) {
return setElRender(listener.el, listener.bindType, listener.src, 'loaded')
}
let rect = listener.el.getBoundingClientRect()
if ((rect.top < window.innerHeight * Init.preLoad && rect.bottom >= 0) && (rect.left < window.innerWidth * Init.preLoad && rect.right >= 0)) {
render(listener)
}
}
const setElRender = (el, bindType, src, state) => {
// 避免重复render
let stateDone = el.getAttribute('lazy') === 'loaded'
if (stateDone) {
return
}
if (!bindType) {
el.setAttribute('src', src)
} else {
el.setAttribute('style', bindType + ': url(' + src + ')')
}
// 默认会给图片添加有渐显效果的类名
if (state === 'loaded' && (el.className.indexOf('animation__fade') === -1)) {
el.className += ' animation__fade'
}
el.setAttribute('lazy', state)
}
const render = (item) => {
if (item.attempt >= Init.attempt) {
return false
}
item.attempt += 1
loadImageAsync(item)
.then((image) => {
setElRender(item.el, item.bindType, item.src, 'loaded')
imageCache.push(item.src)
Listeners.$remove(item)
})
.catch((error) => {
setElRender(item.el, item.bindType, item.error, 'error')
})
}
const loadImageAsync = (item) => {
return new Promise((resolve, reject) => {
let image = new Image()
image.src = item.src
image.onload = function() {
resolve({
naturalHeight: image.naturalHeight,
naturalWidth: image.naturalWidth,
src: item.src
})
}
image.onerror = function() {
reject()
}
})
}
const componentWillUnmount = (el, binding, vnode, OldVnode) => {
if (!el) {
return
}
for (let i = 0, len = Listeners.length; i < len; i++) {
if (Listeners[i] && Listeners[i].el === el) {
Listeners.splice(i, 1)
}
}
if (Init.hasbind && Listeners.length === 0) {
onListen(window, false)
}
}
const checkElExist = (el) => {
let hasIt = false
Listeners.forEach((item) => {
if (item.el === el) hasIt = true
})
if (hasIt) {
return Vue.nextTick(() => {
lazyLoadHandler()
})
}
return hasIt
}
const addListener = (el, binding, vnode) => {
/* if (el.getAttribute('lazy') === 'loaded') {
return
}
if (checkElExist(el)) {
return
} */
// 跳过不必要刷新
if (binding.value === binding.oldValue) {
return
}
let parentEl = null
let imageSrc = binding.value
let imageLoading = Init.loading
let imageError = Init.error
if (typeof binding.value !== 'string') {
imageSrc = binding.value.src
imageLoading = binding.value.loading || Init.loading
imageError = binding.value.error || Init.error
}
if (binding.modifiers) {
parentEl = window.document.getElementById(Object.keys(binding.modifiers)[0])
}
setElRender(el, binding.arg, imageLoading, 'loading')
vnode.context.$nextTick(() => {
Listeners.push({
bindType: binding.arg,
attempt: 0,
parentEl: parentEl,
el: el,
error: imageError,
src: imageSrc
})
if (Listeners.length > 0 && !Init.hasbind) {
Init.hasbind = true
onListen(window, true)
}
if (parentEl) {
onListen(parentEl, true)
}
lazyLoadHandler()
})
}
Vue.directive('lazy', {
bind: addListener,
update: addListener,
componentUpdated: lazyLoadHandler,
unbind: componentWillUnmount
})
}
当你看到这,我要谢谢你,这篇博客零零散散写了好几回,搁置了小一年了。。。不知道在想啥。。。什么什么都怎么写的都有点乱了。。。烂尾了。。。实在抱歉,下不为例~