vue-lazyload 源码解析

Lazy 类

/src/lazy.js

构造函数

定义变量接收实例化参数。

this.version = '__VUE_LAZYLOAD_VERSION__'
this.mode = modeType.event
this.ListenerQueue = []
this.TargetIndex = 0
this.TargetQueue = []
this.options = {
    // 不用打印debug信息
    silent: silent,
    // 是否绑定dom事件
    dispatchEvent: !!dispatchEvent,
    throttleWait: throttleWait || 200,
    preLoad: preLoad || 1.3,
    preLoadTop: preLoadTop || 0,
    error: error || DEFAULT_URL,
    loading: loading || DEFAULT_URL,
    attempt: attempt || 3,
    // 像素比(常见2或3)
    scale: scale || getDPR(scale),
    ListenEvents: listenEvents || DEFAULT_EVENTS,
    hasbind: false,
    supportWebp: supportWebp(),
    // 过滤懒加载的监听器
    filter: filter || {},
    // 适配器,动态改变元素属性
    adapter: adapter || {},
    // 通过IntersectionObserver监听Viewport
    observer: !!observer,
    observerOptions: observerOptions || DEFAULT_OBSERVER_OPTIONS
}

lazy.js 默认导出一个函数,该函数返回一个 Lazy 类,形成闭包,保持对 Vue 的引用。

// lazy.js
export default function (Vue) {
    return class Lazy {
        constructor (options = {}) {
            // init...
        }
    }
}

// index.js
import Lazy from './lazy.js';

export default {
    install (Vue, options = {}) {
        const LazyClass = lazy(Vue);
        const lazy = new LazyClass(options);
    }
}

判断是否支持Webp图片

const inBrowser = typeof window !== 'undefined' && window !== null;

function supportWebp() {
    if (!inBrowser) return false;
    let support = true;
    try {
        const elem = document.createElement('canvas');
        if (!!(elem.getContext && elem.getContext('2d'))) {
            support = elem.toDataURL('image/webp').indexOf('data:image/webp') === 0;
        }
    } catch (err) {
        support = false;
    }
    return support;
}

Lazy 具体做了什么

  1. 构造函数初始化,配置参数定义 options 对象接收;

  2. 调用私有方法_initEvent,图片加载状态触发机制,once 添加仅执行一次的方法,emit 通知触发;

    // Event 结构
    Event = {
        listeners: {
            loading: [],
            loaded: [],
            error: []
        }
    }
    
  3. 实例 ImageCache,提供一个图片缓存容器;

  4. 设置元素进入 viewport 的监听模式,通过 listenEvents(scroll,wheel,touchmove等) 还是 通过 API IntersectionObserver,默认 listenEvents。遍历 TargetQueue 数组内所有元素,绑定触发事件,以及方法 lazyLoadHandler;

  5. 暴露了 add 方法,对应 Vue 自定义指定的bind

    add (el, binding, vnode) {
        // 如果绑定的元素已在监听元素容器中,则触发更新,并加载image
        if (some(this.ListenerQueue, item => item.el === el)) {
            this.update(el, binding);
            return Vue.nextTick(this.lazyLoadHandler);
        }
        ...
        Vue.nextTick(() => {
            // 通过img标签的srcset属性以及像素比,选择适配屏幕的图片资源
            src = getBestSelectionFromSrcset(el, this.options.scale) || src;
            // 如果存在通过 IntersectionObserver 监听 viewport,则绑定该元素
            this._observer && this._observer.observe(el);
            
            // 定义父元素用于触发监听事件
            // 方法1:自定义
            /**
             * 
    */ const container = Object.keys(binding.modifiers)[0]; let $parent if (container) { $parent = vnode.context.$refs[container]; $parent = $parent ? $parent.el || $parent : document.getElementById(container); } // 方法2:向上遍历,直至找到样式overflow为auto或scroll的祖先元素(不超过body、html) if (!$parent) { $parent = scrollParent(el); } // 收集监听者实例 const newListener = new ReactiveListener({ ... }); this.ListenerQueue.push(newListener); if (inBrowser) { // TargetQueue 收集需要触发 listenEvents 事件的元素 // 除了监听$parent还监听了window // 元素添加 el.addEventListener(ev, lazyLoadHandler) this._addListenerTarget(window); this._addListenerTarget($parent); } // 加载已满足 viewport 算法的图片 this.lazyLoadHandler(); }) }
  1. 暴露了 update 方法,对应 Vue 自定义指令的update

  2. 暴露了 lazyLoadHandler 方法,对应 Vue 自定义指令的componentUpdated——遍历 ListenerQueue, 图片元素进入 viewport 后,进行图片加载;

  3. 暴露了 remove 方法,对应 Vue 自定义指令的unbind;

  4. 在入口 index.js 文件中 Vue.prototype.Lazyload** 可以获取到对应属性和方法。

ReactiveListener 类

/src/listener.js

构造函数

定义变量接收实例化参数。

this.el = el
// 预期的图片地址
this.src = src
// 加载异常的图片
this.error = error
// 加载中的图片
this.loading = loading
// v-lazy:bindType v-lazy:background-image
this.bindType = bindType
this.attempt = 0
this.cors = cors

this.naturalHeight = 0
this.naturalWidth = 0

this.options = options

this.rect = null

this.$parent = $parent
this.elRenderer = elRenderer
this._imageCache = imageCache
this.performanceData = {
    init: Date.now(),
    loadStart: 0,
    loadEnd: 0
}

filter 方法将配置的 filter 对象中的方法执行,接收两个参数,一个为 ReactiveListener 实例,一个为 options 参数对象。

filter () {
    ObjectKeys(this.options.filter).map(key => {
        this.options.filter[key](this, this.options);
    })
}

initState 方法给元素添加 data-set 属性,值为图片地址 src,并且定义了图片状态对象 state 。在 Lazy 中已经根据像素比选择了最适配屏幕的图片,顾这里不需要考虑 srcset 属性。另外,我们自定义指令是 v-lazy,到目前为止,还没有给图片的 src 属性赋值。

this.state = {
    loading: false,
    error: false,
    loaded: false,
    rendered: false
}

render 方法,是在 Lazy 中实例化 ReactiveListener 时传递过来的参数。

// lazy.js
const newListener = new ReactiveListener({
    ...
    elRenderer: this._elRender.bind(this),
    ...
})

_elRenderer (listener, state, cache) {
    if (!listener.el) return;
    const { el, bindType } = listen;
    
    let src
    switch (state) {
        case 'loading':
            src = listener.loading;
            break;
        case 'error':
            src = listener.error;
            break;
        case 'loaded':
        default:
            src = listener.src;
            break;
    }
    
    if (bindType) {
        // 背景图片
        el.style[bindType] = `url("${src}")`;
    } else if (el.getAttribute('src') !== src) {
        // 在这里正式给图片 src 属性赋值(加载中,加载完成,加载异常)
        el.setAttribute('src', src);
    }
    // 标签自定义 lazy 属性,标记图片加载状态
    el.setAttribute('lazy', state);
    
    // 通过 $on 方法订阅的通知
    // 一般在单个 Vue 文件中通过 this.$Lazyload.$on(name, fn),fn回调函数接收参数为 ReactiveListener 实例
    this.$emit(state, listener, cache);
    
    // adapter 适配器参数一般在 main.js 文件中配置,在图片加载的状态变更时触发,生命周期
    this.options.adapter[state] && this.options.adapter[state](listener, this.options);
}

// listener.js
this.render('loading', state);

render (state, cache) {
    this.elRenderer(this, state, cache);
}

lazyLoadHandler

回过头再来结合 lazy.js 中的 lazyLoadHandler 方法与 ReactiveListener 暴露的方法来看。

// lazy.js
_lazyLoadHandler () {
    const freeList = [];
    // ListenerQueue 中是 ReactiveListener 的实例
    this.ListenerQueue.forEach(listener => {
        if (!listener.el || !listener.el.parentNode) {
            freeList.push(listen);
        }
        const catIn = listener.checkInView();
        if (!catIn) = return;
        listener.load();
    })
    freeList.forEach(item => {
        remove(this.ListenerQueue, item);
        item.$destroy();
    })
}

// listener.js
getRect () {
    this.rect = this.el.getBoundingClientRect();
}

// 结合核心参数 preLoad 算出何时加载
checkInView () {
    this.getRect();
    return (this.rect.top < window.innerHeight * this.options.preLoad && this.rect.bottom > this.options.preLoadTop) && (this.rect.left < window.innerWidth * this.options.preLoad && this.rect.right > 0);
}

// 预期图片加载
load (onFinish = () => {}) {
    // 超出尝试次数且加载失败
    if (this.attempt > this.options.attempt - 1 && this.state.error) {
        return onFinish();
    }
    if (this.state.rendered && this.state.loaded) return;
    //是否有缓存
    if (this._imageCache.has(this.src)) {
        this.state.loaded = true;
        this.render('loaded', true);
        this.state.rendered = true;
        return onFinish();
    }
    // 加载 loading 的图片
    this.renderLoading(() => {
        this.attempt++;
        // 生命周期 beforeLoad
        this.options.adapter['beforeLoad'] && this.options.adapter['beforeLoad'](this, this.options);
        // 记录图片开始加载的时间
        this.record('loadStart');
        // 加载预期图片
        loadImageAsync({
            src: this.src,
            cors: this.cors
        }, data => {
            // resolve
            this.naturalHeight = data.naturalHeight;
            this.naturalWidth = data.naturalWidth;
            this.state.loaded = true;
            this.state.error = false;
            this.record('loadEnd');
            this.render('loaded', false);
            this.state.rendered = true;
            // 添加图片缓存
            this._imageCache.add(this.src);
            onFinish();
        }, err => {
            // reject
            !this.options.silent && console.error(err);
            this.state.error = true;
            this.state.loaded = false;
            this.render('error', false);
        })
    })    
}

LazyContainer 类

/src/lazy-container.js

LazyContainer 的核心是 container 下的选择器selector(默认 img 标签)遍历后调用 lazy 的 add 方法进行绑定,自定义指令 v-lazyload-container。

const imgs = this.getImgs();
imgs.forEach(el => {
    this.lazy.add(el, assign({}, this.binding, {
        value: {
            src: 'dataset' in el ? el.dataset.src : el.getAttribute('data-src'),
            error: ('dataset' in el ? el.dataset.error : el.getAttribute('data-error')) || this.options.error,
            loading: ('dataset' in el ? el.dataset.loading : el.getAttribute('data-loading')) || this.options.loading
            
        }
    }), this.vnode)
})

LazyComponent 类

/src/lazy-component.js

上述实现元素绑定主要是通过自定义指令 v-lazyv-lazy-container。那么 LazyComponent 则是通过注册的 lazy-component 组件,完成绑定,默认渲染成为 div 标签,作为 img 的容器。

// lazy-component.js
mounted () {
    this.el = this.$el;
    lazy.addLazyBox(this);
    lazy.lazyLoadHandler();
},
// 自定义指令,调用的是 ReactiveListener 实例的 load 方法
// 自定义组件,调用的 methods 中的 load 方法
// 如果需要更高的定制化,推荐使用自定义指令
methods: {
    getRect () {},
    checkInView () {},
    load () {}
}

// lazy.js
addLazyBox (vm) {
    // 添加至图片加载实例容器
    this.listenerQueue.push(vm);
    if (inBrowser) {
        // 元素添加 el.addEventListener(ev, lazyLoadHandler)
        this._addListenerTarget(window);
        this._observer && this._observer.observe(vm.el);
        if (vm.$el && vm.$em.parentNode) {
            this._addListenerTarget(vm.$el.parentNode);
        }
    }
}

LazyImage 类

/src/lazy-image.js

通 LazyComponent 组件,只不过 LazyImage 注册的 lazy-image 组件,渲染成的是 img 标签,多了 src 属性。

核心过程

通过自定义指令 v-lazy 将设置背景图的元素或者 img元素,通过 _addListenerTarget 方法收集与数组 TargetQueue 中,并遍历触发懒加载的方法,addEventListener 绑定在该元素上,触发的事件为 lazyLoadHandler

在需要懒加载的元素上设置属性 data-src,这是期望的图片地址(filter 配置项可以预先过滤赋值),元素上自定义 lazyLoad 表示图片状态(状态变更后,adapter 中触发回调);

ListenerQueue 数组中收集的是 ReactiveListener 类的实例,主要是用于懒加载不同状态下的图片加载,loading - loaded - error;

当触发 EventListener 了,执行 lazyLoadHandler 方法,根据算法,进入 viewport 后,ReactiveListener 元素如果与触发元素匹配,则进行图片的加载及渲染。

你可能感兴趣的:(vue-lazyload 源码解析)