最近負責的項目漸漸的由業務型轉向營銷型,營銷內容愈來愈多,圖片也就多了起來。圖片一多起來問題就來了,一上來幾十張圖片加載起來半天都過去了,咋辦?涼拌--懶加載javascript
懶加載也叫延遲加載,本質上就是按需加載,即只有當圖片dom已經在或者即將進入用戶視線範圍內的時候纔去加載對應的dom圖片。前兩年流行的瀑布流其實就是按需加載的一個應用實例。java
在懶加載的模型中,一個頁面能夠氛圍三個區域如圖,視圖區、即將進入視圖區、視圖遠區(即圖中的不加載區)。
當一個圖片在視圖區或者隨着屏幕滾動即將進入視圖區的時候,就開始加載圖片。node
大象裝冰箱,也得分三步,懶加載也同樣
首先,將圖片標籤加入懶加載隊列lazyQueue,而且給圖片加載一個默認圖,通常是個品牌logo或者1px*1px的空白圖片作拉伸。加載默認圖主要是爲了看起來不那麼尷尬,用戶廣泛尷尬耐受力強的能夠不加~
其次,添加屏幕滾動監聽事件,如mousemove、wheel滾動等,實時監聽懶加載隊列lazyQueue中的dom位置變更。這一步算是懶加載的核心了吧。
最後,在監聽器中實時判斷圖片dom的位置,若是已進入加載區的位置就去加載圖片,加載完成後把響應的dom移出lazyQueue。
直接上代碼吧,怎麼感受這麼墨跡。。。app
/* 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 }) }
當你看到這,我要謝謝你,這篇博客零零散散寫了好幾次,擱置了小一年了。。。不知道在想啥。。。什麼什麼都怎麼寫的都有點亂了。。。爛尾了。。。實在抱歉,下不爲例~dom