从Vue-lazyload看图片懒加载
基本实现
监听最外层div的滚动事件,触发滚动时遍历图片检测图片位置,若在可视区内则显示
loadImg() {
var img = this.$refs.lazy.getElementsByClassName("lazyImg");
// 已滚动高度+可视区高度
var top = this.$refs.lazy.scrollTop + this.$refs.lazy.clientHeight;
for(var i = 0; i < img.length; i++) {
if(img[i].offsetTop <= top) { // 在可视区内则显示图片
img[i].src = img[i].getAttribute("data-src");
}
}
},
lazyLoad() {
this.loadImg();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
利用节流函数对代码进行优化
function throttle(func, wait) {
return function() {
var timeout;
var context = this, args = [].slice.call(arguments,1);
if(!timeout) {
setTimeout(function(){
func.apply(context,args);
timeout = null;
}, wait)
}
}
}
lazyLoad() {
this.throttle(this.loadImg, 500)();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
如果用户已经加载之后的图片则不需要加载,所以加一个判断:
lazyLoad() {
if(this.$refs.lazy.scrollTop - this.oldScrollTop < 0) { // 如果向上拉则不做操作
return ;
} else { // 如果向下拉但小于500px则节流加载
this.throttle(this.loadImg, 500)();
}
}
2
3
4
5
6
7
我们的loadImg
方法中每次都是从0号下标开始遍历检查图片,但是在用户上拉操作之后一部分图片已经被加载了,就不需要再次去检查了。我们可以用一个变量len
记录上一次被加载后的最后一个图片,然后修改一下loadImg
loadImg() {
var img = this.$refs.lazy.getElementsByClassName("lazyImg");
var top = this.$refs.lazy.scrollTop + this.$refs.lazy.clientHeight;
// 从len开始检查
for(var i = this.len; i < img.length; i++) {
if(img[i].offsetTop <= top) {
img[i].src = img[i].getAttribute("data-src");
this.len = i; // 更新len
}
}
}
2
3
4
5
6
7
8
9
10
11
12
懒加载的几种方式
图片出现在视窗内的情况: offsetTop < clientHeight + scrollTop
图片出现在视窗内的情况: element.getBoundingClientRect().top < clientHeight
h5的IntersectionObserver方式
- intersectionRatio:目标元素的可见比例,即 intersectionRect 占 boundingClientRect 的比例,完全可见时为 1 ,完全不可见时小于等于 0
具体用法如下:
function getTag(tag) { return Array.from(document.getElementsByTagName(tag)); } var observer = new IntersectionObserver( (changes) => { changes.forEach((change) => { if (change.intersectionRatio > 0) { var img = change.target; img.src = img.dataset.src; observer.unobserve(img); } }) } ) getTag('img').forEach((item) => { observer.observe(item); })
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
vue-lazyload如何实现的
从入口分析:
src/index.js
中主要做了两件事:
- 创建lazy对象并定义lazy指令
const lazy = new LazyClass(options) // 核心函数
Vue.directive('lazy', {
bind: lazy.add.bind(lazy),
update: lazy.update.bind(lazy),
componentUpdated: lazy.lazyLoadHandler.bind(lazy),
unbind: lazy.remove.bind(lazy)
})
2
3
4
5
6
7
Lazy类
// 第一行
this._initEvent()
// 第二行
this.lazyLoadHandler = throttle(this._lazyLoadHandler.bind(this), this.options.throttleWait)
// 第三行
this.setMode(this.options.observer ? modeType.observer : modeType.event)
2
3
4
5
6
7
8
9
第一行对loading、loaded、error事件监听方法的初始化
第二行代码对懒加载处理函数进行了节流处理,这里我们需要关心的地方有懒加载处理函数、节流处理函数
// 懒加载处理函数
// 将监听队列中loaded状态的监听对象取出存放在freeList中并删掉
// 判断未加载的监听对象是否处在预加载位置,如果是则执行load方法。
_lazyLoadHandler () {
const freeList = []
this.ListenerQueue.forEach((listener, index) => {
if (!listener.state.error && listener.state.loaded) {
return freeList.push(listener)
}
// 判断当前监听对象是否在预加载位置,如果是则执行load方法开始加载
const catIn = listener.checkInView()
if (!catIn) return
listener.load()
})
freeList.forEach(vm => remove(this.ListenerQueue, vm))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
第三行设置监听模式,我们通常使用scroll
或者IntersectionObserver
来判断,元素是否进入视图,若进入视图则需为图片加载真实路径。如果使用scroll
则mode
值为event
,如果使用IntersectionObserver
则mode
值为observer
;
scroll
如果使用scroll形式,则调用this._initListen(target.el, true)
这段代码为目标容器添加事件监听。默认监听'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'
这些事件,当事件触发时调用预加载处理函数lazyLoadHandler
IntersectionObserver
IntersectionObserver可以用来监听元素是否进入了设备的可视区域之内,而不需要频繁的计算来做这个判断。
当使用IntersectionObserver模式时,主要做两步处理:
- this._initListen(target.el, false) : 移除目标容器对'scroll', 'wheel', 'mousewheel', 'resize', 'animationend', 'transitionend', 'touchmove'事件的监听。
- this._initIntersectionObserver() 添加IntersectionObserver监听
setMode (mode) {
if (!hasIntersectionObserver && mode === modeType.observer) {
mode = modeType.event
}
this.mode = mode // event or observer
if (mode === modeType.event) {
if (this._observer) {
this.ListenerQueue.forEach(listener => {
this._observer.unobserve(listener.el)
})
this._observer = null
}
this.TargetQueue.forEach(target => {
this._initListen(target.el, true)
})
} else {
this.TargetQueue.forEach(target => {
this._initListen(target.el, false)
})
this._initIntersectionObserver()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
lazy指令
lazy自定义指令中声明的几个钩子函数
- bind: 只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。
- update:所在组件的 VNode 更新时调用,但是可能发生在其子 VNode 更新之前。指令的值可能发生了改变,也可能没有。但是你可以通过比较更新前后的值来忽略不必要的模板更新
- componentUpdated:指令所在组件的 VNode 及其子 VNode 全部更新后调用。
- unbind:只调用一次,指令与元素解绑时调用。
add
lazy.add
方法中的主要逻辑就两点:
- 当前dom若已存在监听队列
ListenerQueue
中,则直接调用this.update
方法并再dom渲染完毕之后执行懒加载处理函数this.lazyLoadHandler
- 若当前dom不存在监听队列中:
- 则创建新的监听对象
newListener
并将其存放在监听队列ListenerQueue
中。 - 设置
window
或$parent
为scroll事件的监听目标对象。 - 执行懒加载处理函数
this.lazyLoadHandler()
。
- 则创建新的监听对象
listener
在ReactiveListener类的构造函数末尾执行了三个方法:
this.filter(): 调用用户传参时定义的filter方法。
this.initState():将图片的真实路径绑定到元素的
data-src
属性上,并为监听对象添加error,loaded,rendered状态。this.render('loading', false): 实际调用的是lazy.js中的
_elRenderer
方法。- 根据传递的状态参数
loading
设置当前图片的路径为loading状态占位图路径。 - 将loading状态绑定到元素的lazy属性上。
- 触发用户监听loading状态上的函数
this.$emit(state, listener, cache)
- 根据传递的状态参数
lazyLoadHandler
懒加载函数干的事情就两点:
- 遍历所有监听对象并删除掉已经加载完毕状态为loaded的listener;
- 遍历所有监听对象并判断当前对象是否处在预加载位置,如果处在预加载位置,则执行监听对象的load方法。
第一点逻辑一目了然,不需要再过多阐述。我们主要了解一下_lazyLoadHandler
中使用到的两个方法。一是判断当前对象是否处在预加载位置的listener.checkInView()
;另一个是监听对象的load方法:listener.load()
;
load (onFinish = noop) {
// 若尝试次数完毕并且对象状态为error,则打印错误提示并结束。
if ((this.attempt > this.options.attempt - 1) && this.state.error) {
if (!this.options.silent) console.log(`VueLazyload log: ${this.src} tried too more than ${this.options.attempt} times`)
onFinish()
return
}
// 若当前对象状态为loaded并且路径已缓存在imageCache中,则调用this.render('loaded', true)渲染dom真实路径。
if (this.state.loaded || imageCache[this.src]) {
this.state.loaded = true
onFinish()
return this.render('loaded', true)
}
// 若以上条件都不成立,则调用renderLoading方法渲染loading状态的图片。
this.renderLoading(() => {
this.attempt++
this.record('loadStart')
loadImageAsync({
src: this.src
}, data => {
this.naturalHeight = data.naturalHeight
this.naturalWidth = data.naturalWidth
this.state.loaded = true
this.state.error = false
this.record('loadEnd')
this.render('loaded', false)
imageCache[this.src] = 1
onFinish()
}, err => {
!this.options.silent && console.error(err)
this.state.error = true
this.state.loaded = false
this.render('error', false)
})
})
}
// renderLoading方法
renderLoading (cb) {
// 异步加载图片
loadImageAsync(
{
src: this.loading
},
data => {
this.render('loading', false)
cb()
},
() => {
// handler `loading image` load failed
cb()
if (!this.options.silent) console.warn(`VueLazyload log: load failed with loading image(${this.loading})`)
}
)
}
// loadImageAsync方法
const loadImageAsync = (item, resolve, reject) => {
let image = new Image()
image.src = item.src
image.onload = function () {
resolve({
naturalHeight: image.naturalHeight,
naturalWidth: image.naturalWidth,
src: image.src
})
}
image.onerror = function (e) {
reject(e)
}
}
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
70
71
72
73
74
75
76
77
总结
原理简述:
- vue-lazyload是通过指令的方式实现的,定义的指令是v-lazy指令
- 指令被bind时会创建一个listener,并将其添加到listener queue里面, 并且搜索target dom节点,为其注册dom事件(如scroll事件)
- 上面的dom事件回调中,会遍历 listener queue里的listener,判断此listener绑定的dom是否处于页面中perload的位置,如果处于则加载异步加载当前图片的资源
- 同时listener会在当前图片加载的过程的loading,loaded,error三种状态触发当前dom渲染的函数,分别渲染三种状态下dom的内容