懒加载的好处

前端领域中,图片懒加载可谓是老生常谈,一方面可以减少服务器带宽流量的消耗,二来合理分配浏览器的请求开销,带来更优的体验。

代码实现

先贴上代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
const LazyLoad = {
install(Vue, options) {
let src = options.default
Vue.directive('lazy', {
bind(el, binding) {
LazyLoad.init(el, binding.value, src)
},
inserted(el) {
if (IntersectionObserver) {
LazyLoad.observe(el)
} else {
LazyLoad.listenerScroll(el)
}
}
})
},
init(el, val, def) {
el.setAttribute('data-src', val)
el.setAttribute('src', def)
},
observe(el) {
var io = new IntersectionObserver(entries => {
let realSrc = el.dataset.src
if (entries[0].isIntersecting) {
if (realSrc)
{
el.src = realSrc
el.removeAttribute('data-src')
}
}
})
io.observe(el)
},
listenerScroll(el) {
let handler = LazyLoad.throttle(LazyLoad.load, 300)
LazyLoad.load(el)
window.addEventListener('scroll', () => {
handler(el)
})
},
load(el) {
let windowHeight = document.documentElement.clientHeight
let elTop = el.getBoundingClientRect().top
let elBtm = el.getBoundingClientRect().bottom
let realSrc = el.dataset.src
if (elTop - windowHeight < 0 && elBtm > 0) {
if (realSrc) {
el.src = realSrc
el.removeAttribute('data-src')
}
}
},
throttle(fn, delay) {
let preTime
return function(...args) {
let currTime = Date.now()
let context = this
if (!preTime) {
preTime = currTime
}
if (currTime - preTime > delay) {
preTime = currTime
fn.apply(context, args)
}
}
}
}

export default LazyLoad

在项目中使用。

1
2
3
4
// 图片懒加载
Vue.use(Lazyload, {
default: DEFAULT_URL // 为加载前的占位图
})

思路分析

判断当前浏览器是否支持 IntersectionObserver API

  1. 如果支持,则创建Intersection Observer对象,并传入监听元素。如果观察对象与根元素相交,即isIntersecting为true,则对src属性赋值
  2. 如果不支持,则监听屏幕的滚动事件,通过节流函数和 getBoundingClientRect API,定时获取元素在屏幕的位置,
    判断在可是范围内,则对src属性赋值

Intersection Observer

IntersectionObserver接口 (从属于Intersection Observer API) 提供了一种异步观察目标元素与其祖先元素或顶级文档视窗(viewport)交叉状态的方法。

  • IntersectionObserver.observe()
    使IntersectionObserver开始监听一个目标元素。
1
2
3
4
5
6
7
8
9
10
var intersectionObserver = new IntersectionObserver(function(entries) {
// If intersectionRatio is 0, the target is out of view
// and we do not need to do anything.
if (entries[0].intersectionRatio <= 0) return;

loadItems(10);
console.log('Loaded new items');
});
// start observing
intersectionObserver.observe(document.querySelector('.scrollerFooter'));

entries 是一个 IntersectionObserverEntry 对象的数组,IntersectionObserverEntry 包换以下元素(来自MDN)

  • boundingClientRect: 返回包含目标元素的边界信息的DOMRectReadOnly. 边界的计算方式与 Element.getBoundingClientRect() 相同
  • intersectionRatio: 返回intersectionRect 与 boundingClientRect 的比例值
  • intersectionRect: 返回一个 DOMRectReadOnly 用来描述根和目标元素的相交区域
  • isIntersecting: 返回一个布尔值, 如果目标元素与交叉区域观察者对象(intersection observer) 的根相交,则返回 true .如果返回 true, 则 IntersectionObserverEntry 描述了变换到交叉时的状态; 如果返回 false, 那么可以由此判断,变换是从交叉状态到非交叉状态
  • rootBounds: 返回一个 DOMRectReadOnly 用来描述交叉区域观察者(intersection observer)中的根
  • target: 与根出现相交区域改变的元素 (Element)
  • time: 返回一个记录从 IntersectionObserver 的时间原点(time origin)到交叉被触发的时间的时间戳(DOMHighResTimeStamp)

Element.getBoundingClientRect()

Element.getBoundingClientRect() 方法返回元素的大小及其相对于视口的位置。

返回的结果是包含完整元素的最小矩形,并且拥有left, top, right, bottom, x, y, width, 和 height这几个以像素为单位的只读属性用于描述整个边框。除了width 和 height 以外的属性是相对于视图窗口的左上角来计算的。

Vue.directive()

钩子函数

  • bind:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。

  • inserted:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。