从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();
}
1
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)();
}
1
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)();
    }
}
1
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
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

懒加载的几种方式

  1. 图片出现在视窗内的情况: offsetTop < clientHeight + scrollTop

  2. 图片出现在视窗内的情况: element.getBoundingClientRect().top < clientHeight

  3. 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)
})
1
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)
1
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))
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

第三行设置监听模式,我们通常使用scroll或者IntersectionObserver来判断,元素是否进入视图,若进入视图则需为图片加载真实路径。如果使用scrollmode值为event,如果使用IntersectionObservermode值为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()
    }
}
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

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)
    }
}
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
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的内容