前言
在实际的项目中还是有很多图片懒加载的需求,尤其是c端的应用,在一些列表页,像商品列表等含有大量图片展示。我们能做的一个优化项就是对未进入当前视口的图片元素,延迟加载,减少一进入页面时的请求数。
图片的懒加载从实现上来说,就是一开始不设置 img 元素的 src,在上滑过程中,当图片滚动到可见时才进行加载,触发 src 设置。
这里暂时只介绍在 vue 中基于自定义指令如何实现,原生 js 或者 react 中大同小异,主要是实现的思路
常用的实现方式:
- 监听
onscroll
滚动事件 - 借助
IntersectionObserver
实现
一、监听 onscroll 事件
通过监听 onscroll
滚动事件,当滚动事件触发时,判断 el 元素是否进入可视区域。
关键是如何判断元素进入可视区域?
我们可以康一康 getBoundingClientRect
方法,MDN 上解释如为:
Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。包含当前元素的 left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性
视口默认为屏幕左上角,加载 5 张图片来看一下打印的 top 值每张图片的宽高为 300px,不难看出 top 值就是元素到屏幕顶部距离,慢慢向上滑动,top 的值是随着滑动距离递减的。于是我们可以得出简单结论:
- 当
top < 当前视口高度时
,可以判断元素进入视口可见区域,获取视口高度可以结合documentElement.clientHeight
和body.clientHeight
,具体的实现看项目的兼容性要求,这里只给出大致思路。
判断元素达到可视区域的代码参考如下:
/**
* 判断元素是否进入可视区域
*/
export const isElementInViewport = el => {
if (typeof el.getBoundingClientRect !== 'function') {
return true
}
const clientHeight = _getClientHeight()
const rect = el.getBoundingClientRect()
return rect.top }
// 获取视口高度
const _getClientHeight = () => {
const dClientHeight = document.documentElement.clientHeight
const bodyClientHeight = document.body.clientHeight
let clientHeight = 0
if (bodyClientHeight && dClientHeight) {
clientHeight = bodyClientHeight } else {
clientHeight = bodyClientHeight > dClientHeight ? bodyClientHeight : dClientHeight
}
return clientHeight
}
监听滚动处理
// 创建 map 来缓存
window.lazyMap = new Map()
// 监听滚动事件,添加节流处理
window.onscroll = throttle(() => {
window.lazyMap.forEach((lazyImg, key) => {
if (isElementInViewport(lazyImg.el)) {
lazyImg.el.src = lazyImg.value.src
lazyImg.value.callback(lazyImg.el)
window.lazyMap.delete(key)
}
})
}, 200)
二、借助 IntersectionObserver
过去,要检测一个元素是否可见或者两个元素是否相交并不容易,很多解决办法不可靠或性能很差,就比如第一种方法,需要监听页面滚动,对性能多少会有点影响
而现在 Intersection Observer API
提供了一种异步检测目标元素与祖先元素或 viewport
相交情况变化的方法。
相交检测可以发挥作用的地方:
- 图片懒加载——当图片滚动到可见时才进行加载
- 内容无限滚动——也就是用户滚动到接近内容底部时直接加载更多,而无需用户操作翻页,给用户一种网页可以无限滚动的错觉
- 检测广告的曝光情况——为了计算广告收益,需要知道广告元素的曝光情况
- 在用户看见某个区域时执行任务或播放动画
关于 IntersectionObserver
的更多说明和用法 传送门
这里用到的一个关键属性 isIntersecting
,这是一个布尔值,指示目标元素是否转换为交集状态(true)或脱离交集状态(false)。当相交时,这个属性的值就为 true。相比之前的是否进入可视区域的判断,简单了不是一点半点?
核心代码为
window.observer = new IntersectionObserver(entries => {
entries.forEach(entry => {
let lazyImage = entry.target
// 判断是否相交
if (entry.isIntersecting) {
const src = lazyImage.getAttribute('src')
lazyImage.src = src
lazyImage.style.opacity = 1
lazyImage.style.display = 'block'
// 移除监听
window.observer.unobserve(lazyImage)
}
})
})
自定义指令
好了,说回自定义指令,如果对自定义指令的用法不太熟悉请参考 Vue 官方文档 传送门
主要是要利用好这几个钩子函数:
- bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- 比如监听
onerror
事件,加载失败后设置 default src。 - 设置
onscroll
监听 IntersectionObserver
的初始化
- 比如监听
- inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。
onscroll
方案,如果 el isInViewport,则设置 src,否则将el
和binding
添加到lazyMap
IntersectionObserver
方案只需要调用observe
监听目标对象
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- 同 inserted,只不过需要做一次 src 的判断,如果
value
和oldValue
一致,不继续往下执行
- 同 inserted,只不过需要做一次 src 的判断,如果
- unbind:只调用一次,指令与元素解绑时调用。
onscroll
方案, 将当前目标元素从lazyMap
移除IntersectionObserver
方案,调用unobserve
移除目标对象监听
最终效果
总结
存在必有其合理性,就目前来说,两种方案都有它们的优缺点
方案 | 优点 | 缺点 |
---|---|---|
基于 onscroll | 兼容性好 | 性能不佳,需要持续监听滚动事件;实现不太优雅,代码量大 |
IntersectionObserver | 性能佳,实现优雅,代码量少 | 兼容性差,IE 完全不支持,在 safari 也需要 12 以上才支持 |
MDN 这篇文章上对这点说得很好,有兴趣的可以看下 传送门
最后
如有不太合理的地方,希望及时指出,多多交流~
讲的总是空洞的,实践出真知,查看代码请各位童鞋移步 github,别忘记点个赞哟 ?
项目地址 ? :https://github.com/MrLeihe/vue-lazy-img
参考资料
- MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/IntersectionObserver
- MDN:https://developer.mozilla.org/zh-CN/docs/Web/API/Intersection_Observer_API
- 自定义指令:https://cn.vuejs.org/v2/guide/custom-directive.html