文章链接:https://mp.weixin.qq.com/s/OO7jVd2kIlkRtNNaRjGLuQ
方便复用,封装成一个插件:
// lazy.js
export const lazyPlugin = {
install (app, options = {}) {
const lazy = new Lazy(options)
app.directive('lazy', {
inserted: lazy.add.bind(lazy)
})
}
}
使用bind
方法返回一个新的函数,且该函数内部的this
指向Lazy
实例,全局只有一个Lazy
实例
Vue3
使用mounted
钩子函数,Vue2
使用inserted
main.js
中引入
import Vue from 'vue'
import App from './App.vue'
import { lazyPlugin } from './/plugins/ImageLoader'
Vue.config.productionTip = false
Vue.use(lazyPlugin)
new Vue({
render: h => h(App),
}).$mount('#app')
构建图片处理类,用于控制图片加载
// 状态常量,用于指示图片的加载状态
export const State = {
loading: 0,
loaded: 1,
error: 2
}
export class ImageManager {
constructor(options) {
this.el = options.el // 图片DOM,img元素
this.src = options.src // 图片地址
this.state = State.loading // 初始为“未加载”
this.loading = options.loading // 用于占位的Loading图片
this.error = options.error
// 初始化指向一张空白图
this.render(this.loading)
}
render (src) {
this.el.setAttribute('src', src)
}
load(next) {
if (this.state > State.loading) {
// 如果已经加载完图片
return
}
this.renderSrc(next)
}
renderSrc(next) {
loadImage(this.src).then(() => {
// 加载完之后,修改当前图片处理器实例的状态
this.state = State.loaded
// 重新设置新的真正加载的图片
this.render(this.src)
next && next()
}).catch(e => {
this.state = State.error
console.log('加载图片失败:', this.src, e)
next && next()
})
}
}
加载图片的方法:
export function loadImage (src) {
// 使用一个Promise异步
// 等待图片加载完成,onload 事件执行,Promise也就执行了
return new Promise((resolve, reject) => {
const image = new Image()
image.onload = () => {
resolve()
dispose()
}
image.onerror = () => {
reject()
dispose()
}
image.src = src
function dispose () {
image.onload = image.onerror = null
}
})
}
const DEFAULT_URL = 'https://www.bing.com/images/search?q=%e5%93%88%e5%a3%ab%e5%a5%87&id=7EA1C608564AA243D3767286CF84AEE79F5081F6&FORM=IQFRBA'
class Lazy {
constructor (options) {
// 图片处理类实例的队列
this.managerQueue = []
// 添加观察者
this.initIntersectionObserver()
this.loading = options.loading || DEFAULT_URL
this.error = options.error || DEFAULT_URL
}
add (el, binding) {
// value 即为绑定命令的值
const src = binding.value
const manager = new ImageManager({
el,
src,
loading: this.loading,
error: this.error
})
this.managerQueue.push(manager)
this.observer.observe(el)
}
initIntersectionObserver () {
this.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
if (entry.isIntersecting) {
// 已经交叉
const manager = this.managerQueue.find(manager => manager.el === entry.target)
if (manager) {
if (manager.state === State.loaded) {
this.removeManager(manager)
return
}
manager.load()
}
}
})
}, {
rootMargin: '0px',
threshold: 0, // 交叉的阈值
})
}
removeManager (manager) {
const idx = this.managerQueue.indexOf(manager)
if (idx > -1) {
this.managerQueue.splice(idx, 1)
}
if (this.observer) {
this.observer.unobserve(manager.el)
}
}
}
主要是使用了IntersectionObserver
API创建DOM
观察者,当观察的元素进入可视区的比例大于threshold
,就会执行回调函数,entries
是所有被观察的DOM
元素,entry
是单个元素在可视区的交叉信息,isIntersecting
为true
表示已经进入可视区,且达到阈值(threshold
)
然后加载图片,如该图片已经加载完成,便从managerQueue
删除该图片管理类实例,并取消该元素的观察
class Lazy {
...
remove(el) {
const manager = this.managerQueue.find((manager) => {
return manager.el === el
})
if (manager) {
this.removeManager(manager)
}
}
}
export const lazyPlugin = {
install(app, options = {}) {
const lazy = new Lazy(options)
app.directive('lazy', {
inserted: lazy.add.bind(lazy),
unbind: lazy.remove.bind(lazy),
})
}
}
unbind
:只调用一次,指令与元素解绑时调用
Vue3
对应的是remove
钩子函数
v-lazy
的绑定的图片地址动态改变时,应该重新设置src
class ImageManager {
...
update (src) {
const curSrc = this.src
if (curSrc !== src) {
this.src = src
this.state = State.loading
}
}
}
constructor(options) {
...
// 添加一个队列,用于记录所有ImageManager实例
this._staticQueue = []
...
}
add(el, binding) {
...
this._staticQueue.push(manager)
...
}
update (el, binding) {
const src = binding.value
const manager = this._staticQueue.find(manager => manager.el === el)
if (manager.state > 0) {
// 已经加载完
manager.update(src)
// 再添加进队列,并监听
this.managerQueue.push(manager)
this.observer.observe(el)
} else {
// 还未加载,直接修改
manager.update(src)
}
}
remove(el) {
const manager = this.managerQueue.find((manager) => {
return manager.el === el
})
if (manager) {
// 解除绑定时,同样需要删除记录
this._staticQueue = this._staticQueue.filter(ins => ins === manager)
this.removeManager(manager)
}
}
URL
缓存优化- 在`Lazy`中建立的集合类型作为缓存,创建图片管理实例时传入
- 在`ImageManager`的`load`方法中,判断是否存在缓存,存在:修改该图片管理实例的加载状态;不存在:正常加载
- 加载完图片后,添加缓存
class Lazy {
constructor(options) {
...
this.cache = new Set()
...
}
add(el, binding) {
const src = binding.value
const manager = new ImageManager({
el,
src,
loading: this.loading,
error: this.error,
cache: this.cache,
})
this._staticQueue.push(manager)
this.managerQueue.push(manager)
this.observer.observe(el)
}
}
class ImageManager {
...
load(next) {
if (this.state > State.loading) {
// 如果已经加载完图片
return
}
// 先查看是否有缓存
if (this.cache.has(this.src)) {
// 缓存中存在,修改图片加载状态
this.state = State.loaded
// 直接加载
this.render(this.src)
}
this.renderSrc(next)
}
renderSrc(next) {
loadImage(this.src).then(() => {
this.state = State.loaded
// 重新设置新的真正加载的图片
this.render(this.src)
// 加载完后,添加进缓存
this.cache.add(this.src)
next && next()
}).catch(e => {
this.state = State.error
this.cache.add(this.src)
this.render(this.error)
console.log('加载图片失败:', this.src, e)
next && next()
})
}
}
IntersectionObserver
API适配// Lazy.js
function throttle(callback, delay) {
let previous;
return (...args) => {
// previous 第一次为 undefined
if(!previous || Date.now() - previous > delay){
callback.apply(null,args)
previous = Date.now()
}
}
}
class Lazy {
constructor(options) {
this.managerQueue = []
this._staticQueue = []
this.cache = new Set()
this.__windowHeight = document.documentElement.clientHeight
this.loading = options.loading || DEFAULT_URL
this.error = options.error || DEFAULT_URL
// 添加全局的滚动事件
this.initListenerScroll()
}
add(el, binding) {
const src = binding.value
const manager = new ImageManager({
el,
src,
loading: this.loading,
error: this.error,
cache: this.cache,
})
this._staticQueue.push(manager)
this.managerQueue.push(manager)
this.load()
}
load () {
// 过滤掉已经加载完成的、加载错误的
this.managerQueue = this.managerQueue.filter(manager => manager.state === State.loading)
this.managerQueue.forEach(manager => this.judgeIsIntersecting(manager.el) && manager.load())
}
judgeIsIntersecting (el) {
const top = el.getBoundingClientRect().top
const bottom = el.getBoundingClientRect().bottom
let IsIntersecting = false
// 三种情况
if (bottom > 0 && top < 0) {
// 1. 从上往下划进可视区,部分交叉
IsIntersecting = true
} else if (top > 0 && bottom <= this.__windowHeight) {
// 2. 完全进入可视区
IsIntersecting = true
} else if (top < this.__windowHeight && bottom > this.__windowHeight) {
// 3. 从下往上划进可视区,部分交叉
IsIntersecting = true
}
return IsIntersecting
}
initListenerScroll() {
const throttleLoad = throttle(this.load.bind(this), 100)
window.addEventListener('scroll', () => {
throttleLoad()
})
}
update (el, binding) {
const src = binding.value
const manager = this._staticQueue.find(manager => manager.el === el)
if (manager.state > 0) {
// 已经加载完
manager.update(src)
// 再添加进队列,并监听
this.managerQueue.push(manager)
} else {
// 还未加载,直接修改
manager.update(src)
}
}
remove(el) {
const manager = this.managerQueue.find((manager) => {
return manager.el === el
})
if (manager) {
this._staticQueue = this._staticQueue.filter(ins => ins === manager)
}
}
}