什么是懒加载
懒加载是针对图片加载时机的优化,适用于图片量比较大的使用场景,当页面初始化时一次性加载所有图片容易造成白屏、卡顿的现象,懒加载的实现就是先加载可视区域的图片,不可见的图片等变为可见时才开始加载。
实现一个懒加载指令
了解了懒加载,我们来自己写一个自定义的懒加载指令吧,目标是实现元素可见自动加载img或者元素背景图
实现思路
- dom绑定指令时,将指令绑定的src值设置到元素的data-src属性上
- 浏览器支持IntersectionObserver,支持则生成观察者实例,观察dom是否可见
- 浏览器不支持IntersectionObserver,监听scroll事件,根据元素的位置判断dom是否可见
- 元素可见时,如果是img元素设置元素的真实src, 其他元素则设置真实的backgroundImage, 删除data-src属性,取消对元素可见性的观察,取消scroll事件的监听
- 元素绑定的src更新时,判断dom 是否有data-src属性,有则没有加载过,直接替换data-src为新的src值,没有则判断元素是否可见,元素可见加载新的src,不可见则设置data-src为新的src值,重新观察dom可见性
- 元素unbind时,取消可见线观察,取消事件监听防止内存泄漏
lazy.ts
import { throttle } from 'g/utils'
/**
* Usage:
*
* In template:
*
* // not image tag
* <div v-lazy="url"></div>
* // image tag
* <img v-lazy="src">
*/
// 保存观察dome可见性的观察器实例,方便图片加载后取消对dom是否可见的观察
let ob: any = null
// 保存scroll事件的回调,方便图片加载后取消监听
const handleStore: any = new WeakMap()
const lazyDirective = {
bind(el, binding) {
const src = binding.value
if (!src || typeof src !== 'string') return
el.setAttribute('data-src', src)
},
inserted(el) {
observeEl(el)
},
update(el, binding) {
if (binding.value === binding.oldValue) return
const src = el.getAttribute('data-src')
if (src) {
// 未加载过的图片src变化,直接修改data-src属性
el.setAttribute('data-src', binding.value)
} else {
// 加载过的图片src变化,判断是否可见,dom可见加载新的src对应图片,dom不可见修改data-src属性,dom设置可见性观察
if (isVisible(el)) {
if (el.tagName === 'IMG') {
el.setAttribute('src', binding.value)
} else {
el.style.backgroundImage = `url(${binding.value})`
}
} else {
el.setAttribute('data-src', binding.value)
observeEl(el)
}
}
},
unbind(el) {
unobserve(el)
}
}
const observeEl = (el: HTMLElement) => {
if ('IntersectionObserver' in window) {
if (!ob) {
ob = new IntersectionObserver(entries => {
entries.forEach(entry => {
// 元素可见
if (entry.isIntersecting) {
loadImage(entry.target as HTMLElement)
}
})
})
}
ob.observe(el)
} else {
lazyLoad(el)
// 滚动事件节流,避免频繁触发
const handle: any = throttle(lazyLoad, 200)
const handleCallback = () => {
handle(el)
}
handleStore.set(el, handleCallback)
window.addEventListener('wheel', handleCallback)
}
}
const isVisible = (el: HTMLElement) => {
const windowHeight = window.innerHeight
const { top, bottom } = el.getBoundingClientRect()
return top - windowHeight < 0 && bottom > 0
}
const lazyLoad = (el: HTMLElement) => {
if (isVisible(el)) {
loadImage(el)
}
}
const loadImage = (el: HTMLElement) => {
const src = el.getAttribute('data-src')
if (!src) return
if (el.tagName === 'IMG') {
// 加载真实的src
el.setAttribute('src', src)
} else {
// 加载真实的backgroundImage
el.style.backgroundImage = `url(${src})`
}
el.removeAttribute('data-src')
unobserve(el)
}
const unobserve = (el: HTMLElement) => {
if (ob) {
// 取消观察el的可见性
ob.unobserve(el)
} else {
// 取消事件订阅
const handleCallback = handleStore.get(el)
if (handleCallback) {
window.removeEventListener('wheel', handleCallback)
handleStore.delete(el)
}
}
}
export default lazyDirective
utils.ts
export const throttle = (
func: (...args: any[]) => any,
wait: number,
immediate: boolean = false
) => {
let timeout: number | undefined
return function(this: any) {
const context = this
const args = arguments
const later = () => {
timeout = void 0
if (!immediate) {
// @ts-ignore
func.apply(context, args)
}
}
if (immediate && !timeout) {
func.apply(context, args)
}
if (!timeout) {
timeout = window.setTimeout(later, wait)
}
}
}
使用
import lazy from 'g/directive/lazy'
Vue.directive('lazy', lazy)
<img v-lazy="src">
<div v-lazy="url"></div>