关于vue.js:Vue3-的图片懒加载自定义指令

13次阅读

共计 6210 个字符,预计需要花费 16 分钟才能阅读完成。

以下文章来源于老黄的前端私房菜,作者黄轶黄老师

背景

家喻户晓,Vue.js 的核心思想是数据驱动 + 组件化,通常咱们开发页面的过程就是在编写一些组件,并且通过批改数据的形式来驱动组件的从新渲染。在这个过程中,咱们不须要去手动操作 DOM。

然而在有些场景下,咱们还是防止不了要操作 DOM。因为 Vue.js 框架接管了 DOM 元素的创立和更新的过程,因而它能够在 DOM 元素的生命周期内注入用户的代码,于是 Vue.js 设计并提供了自定义指令,容许用户进行一些底层的 DOM 操作。

举个理论的例子——图片懒加载。图片懒加载是一种常见性能优化的形式,因为它只去加载可视区域图片,能缩小很多不必要的申请,极大的晋升用户体验。

而图片懒加载的实现原理也非常简单,在图片没进入可视区域的时候,咱们只须要让 img 标签的 src 属性指向一张默认图片,在它进入可视区后,再替换它的 src 指向实在图片地址即可。

如果咱们想在 Vue.js 的我的项目中实现图片懒加载,那么用自定义指令就再适合不过了,那么接下来就让我手把手带你用 Vue3 去实现一个图片懒加载的自定义指令 v-lazy。

插件
为了让这个指令不便地给多个我的项目应用,咱们把它做成一个插件:

const lazyPlugin = {
install (app, options) {

app.directive('lazy', {// 指令对象})

}
}

export default lazyPlugin
而后在我的项目中援用它:

import {createApp} from ‘vue’
import App from ‘./App.vue’
import lazyPlugin from ‘vue3-lazy’

createApp(App).use(lazyPlugin, {
// 增加一些配置参数
})
通常一个 Vue3 的插件会裸露 install 函数,当 app 实例 use 该插件时,就会执行该函数。在 install 函数外部,通过 app.directive 去注册一个全局指令,这样就能够在组件中应用它们了。

指令的实现
接下来咱们要做的就是实现该指令对象,一个指令定义对象能够提供多个钩子函数,比方 mounted、updated、unmounted 等,咱们能够在适合的钩子函数中编写相应的代码来实现需求。

在编写代码前,咱们无妨思考一下实现图片懒加载的几个关键步骤。

图片的治理
治理图片的 DOM、实在的 src、预加载的 url、加载的状态以及图片的加载。

可视区的判断
判断图片是否进入可视区域。

对于图片的治理,咱们设计了 ImageManager 类:

const State = {
loading: 0,
loaded: 1,
error: 2
}

export class ImageManager {
constructor(options) {

this.el = options.el
this.src = options.src
this.state = State.loading
this.loading = options.loading
this.error = options.error

this.render(this.loading)

}
render() {

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
  this.render(this.error)
  console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)
  next && next()})

}
}

export default function loadImage (src) {
return new Promise((resolve, reject) => {

const image = new Image()

image.onload = function () {resolve()
  dispose()}

image.onerror = function (e) {reject(e)
  dispose()}

image.src = src

function dispose () {image.onload = image.onerror = null}

})
}
首先,对于图片而言,它有三种状态,加载中、加载实现和加载失败。

当 ImageManager 实例化的时候,除了初始化一些数据,还会把它对应的 img 标签的 src 执行加载中的图片 loading,这就相当于默认加载的图片。

当执行 ImageManager 对象的 load 办法时,就会判断图片的状态,如果依然在加载中,则去加载它的实在 src,这里用到了 loadImage 图片预加载技术实现去申请 src 图片,胜利后再替换 img 标签的 src,并批改状态,这样就实现了图片实在地址的加载。

有了图片管理器,接下来咱们就须要实现可视区的判断以及对多个图片的管理器的治理,设计 Lazy 类:

const DEFAULT_URL = ‘data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7’

export default class Lazy {
constructor(options) {

this.managerQueue = []
this.initIntersectionObserver()

this.loading = options.loading || DEFAULT_URL
this.error = options.error || DEFAULT_URL

}
add(el, binding) {

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) => {return manager.el === entry.target})
      if (manager) {if (manager.state === State.loaded) {this.removeManager(manager)
          return
        }
        manager.load()}
    }
  })
}, {
  rootMargin: '0px',
  threshold: 0
})

}
removeManager(manager) {

const index = this.managerQueue.indexOf(manager)
if (index > -1) {this.managerQueue.splice(index, 1)
}
if (this.observer) {this.observer.unobserve(manager.el)
}

}
}

const lazyPlugin = {
install (app, options) {

const lazy = new Lazy(options)

app.directive('lazy', {mounted: lazy.add.bind(lazy)
})

}
}
这样每当图片元素绑定 v-lazy 指令,且在 mounted 钩子函数执行的时候,就会执行 Lazy 对象的 add 办法,其中第一个参数 el 对应的就是图片对应的 DOM 元素对象,第二个参数 binding 就是指令对象绑定的值,比方:

<img class=”avatar” v-lazy=”item.pic”>
其中 item.pic 对应的就是指令绑定的值,因而通过 binding.value 就能够获取到图片的实在地址。

有了图片的 DOM 元素对象以及实在图片地址后,就能够依据它们创立一个图片管理器对象,并增加到 managerQueue 中,同时对该图片 DOM 元素进行可视区的察看。

而对于图片进入可视区的判断,次要利用了 IntersectionObserver API,它对应的回调函数的参数 entries,是 IntersectionObserverEntry 对象数组。当观测的元素可见比例超过指定阈值时,就会执行该回调函数,对 entries 进行遍历,拿到每一个 entry,而后判断 entry.isIntersecting 是否为 true,如果是则阐明 entry 对象对应的 DOM 元素进入了可视区。

而后就依据 DOM 元素的比对从 managerQueue 中找到对应的 manager,并且判断它对应图片的加载状态。

如果图片是加载中的状态,则此时执行 manager.load 函数去实现实在图片的加载;如果是已加载状态,则间接从 managerQueue 中移除其对应的管理器,并且进行对图片 DOM 元素的察看。

目前,咱们实现了图片元素挂载到页面后,延时加载的一系列解决。不过,当元素从页面卸载后,也须要执行一些清理的操作:

export default class Lazy {
remove(el) {

const manager = this.managerQueue.find((manager) => {return manager.el === el})
if (manager) {this.removeManager(manager)
}

}
}

const lazyPlugin = {
install (app, options) {

const lazy = new Lazy(options)

app.directive('lazy', {mounted: lazy.add.bind(lazy),
  remove: lazy.remove.bind(lazy)
})

}
}
当元素被卸载后,其对应的图片管理器也会从 managerQueue 中被移除,并且进行对图片 DOM 元素的察看。

此外,如果动静批改了 v-lazy 指令绑定的值,也就是实在图片的申请地址,那么指令外部也应该做对应的批改:

export default class ImageManager {
update (src) {

const currentSrc = this.src
if (src !== currentSrc) {
  this.src = src
  this.state = State.loading
}

}
}

export default class Lazy {
update (el, binding) {

const src = binding.value
const manager = this.managerQueue.find((manager) => {return manager.el === el})
if (manager) {manager.update(src)
}

}
}

const lazyPlugin = {
install (app, options) {

const lazy = new Lazy(options)

app.directive('lazy', {mounted: lazy.add.bind(lazy),
  remove: lazy.remove.bind(lazy),
  update: lazy.update.bind(lazy)
})

}
}
至此,咱们曾经实现了一个简略的图片懒加载指令,在这个根底上,还能做一些优化吗?

指令的优化
在实现图片的实在 url 的加载过程中,咱们应用了 loadImage 做图片预加载,那么显然对于雷同 url 的多张图片,预加载只须要做一次即可。

为了实现上述需要,咱们能够在 Lazy 模块外部创立一个缓存 cache:

export default class Lazy {
constructor(options) {

// ...
this.cache = new Set()

}
}
而后在创立 ImageManager 实例的时候,把该缓存传入:

const manager = new ImageManager({
el,
src,
loading: this.loading,
error: this.error,
cache: this.cache
})
而后对 ImageManager 做如下批改:

export default class ImageManager {
load(next) {

if (this.state > State.loading) {return}
if (this.cache.has(this.src)) {
  this.state = State.loaded
  this.render(this.src)
  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
  this.cache.add(this.src)
  this.render(this.error)
  console.warn(`load failed with src image(${this.src}) and the error msg is ${e.message}`)
  next && next()})  

}
}
在每次执行 load 前从缓存中判断是否已存在,而后在执行 loadImage 预加载图片胜利后更新缓存。

通过这种空间换工夫的伎俩,就防止了一些反复的 url 申请,达到了优化性能的目标。

总结
懒加载图片指令残缺的指令实现,能够在 vue3-lazy 中查看。

懒加载图片指令的外围是利用了 IntersectionObserver API 来判断图片是否进入可视区,该个性在古代浏览器中都反对,但 IE 浏览器不反对,此时能够通过监听图片可滚动父元素的一些事件如 scroll、resize 等,而后通过一些 DOM 计算来判断图片元素是否进入可视区。不过 Vue3 曾经明确不再反对 IE,那么仅仅应用 IntersectionObserver API 就足够了。

除了懒加载图片自定义指令中用到的钩子函数,Vue3 的自定义指令还提供了一些其它的钩子函数,你将来在开发自定义指令时,能够去查阅它的文档,在适宜的钩子函数去编写相应的代码逻辑。

相干链接

[1] IntersectionObserver: https://developer.mozilla.org…

[2] vue3-lazy: https://github.com/ustbhuangy…

[3]《Vue3 开发高质量音乐 Web app》:https://coding.imooc.com/clas…

[4] Vue3 自定义指令文档: https://v3.cn.vuejs.org/guide…

正文完
 0