preface

We all know that lazy image loading is the most basic way to optimize the performance of the front end, but just using tools is not enough, in this increasingly curly circle, building wheels is the way to go. Let’s start by looking at how we use it, introducing plug-ins, using plug-ins.

<script src="./5- lazy loading.js"></script>
Vue.use(VueLazyLoad, {
    preload: 1.3,
    loading,
    error,
})
Copy the code

Now the template can use v-lazy, and that’s it.

<li v-for="img in imglist" :key="img.id">
    <img v-lazy="img.src" alt="">
</li>
Copy the code

Let’s consider how to implement plug-ins based on how we use them.

Implementation approach

Add all objects that need lazy loading to a container, and then add a scroll event to the nearest scrollable parent box. When the parent box rolls, the container is traversed to determine whether the object is in the viewable area, and if it is, the image is loaded.

The implementation process

Create the plug-in

Write all lazy loading-related logic into a class to extend functionality.

const VueLazyLoad = {
    install(Vue, options){
        // Encapsulate lazy logic in a class
        const lazyClass = Lazy(Vue);
        const lazy = newlazyClass(options); }}Copy the code
const Lazy = (Vue) = > {
    // You can extend other features here...

    // Encapsulates lazy-loaded logic
    return class LazyClass{
        constructor(options){
            this.options = options; }}}Copy the code

V – lazy instructions

Vue.nexttick () will execute our logic after the page has been rendered, which is related to Vue’s asynchronous rendering. Unfamiliar words recommended to see the previous vUE source article. The first time the V-lazy directive is called, a rolling event is added to the parent element. Also, every time the V-lazy directive is used within the IMG tag, we create a ReactiveListener object for it, indicating that the image is relevant.

const VueLazyLoad = {
    install(Vue, options){
        // Encapsulate lazy logic in a class
        const lazyClass = Lazy(Vue);
        const lazy = new lazyClass(options);
        Vue.directive('lazy', {
            bind: lazy.add.bind(lazy),
        })
    }
}

const Lazy = (Vue) = > {
    // You can extend other features here...

    // Encapsulates lazy-loaded logic
    return class LazyClass{
        constructor(options){
            this.options = options;
            this.hasAddScrollListener = false;
            this.queueListener = [];
        }

        add(el, bindings, vnode, oldVnode){
            // Find the parent element, listen for the parent element to scroll events, custom instruction bind does not get el, so use nextTick
            Vue.nextTick(() = > {
                const parentNode = getScrollParent(el);
                if(parentNode && !this.hasAddScrollListener){
                    this.hasAddScrollListener = true;
                    parentNode.addEventListener('scroll', debounce(this.scrollHandler.bind(this), 100));
                }
                // Add each image to the queue
                const listener = new ReactiveListener({
                    el: el,
                    src: bindings.value,
                    options: this.options,
                    elRender: this.elRender,
                });
                this.queueListener.push(listener);
                // Determine if the image is visible for the first time
                this.scrollHandler()
            })
        }
    }
}
Copy the code

ReactiveListener object

class ReactiveListener{
    constructor({el, src, options, elRender}){
        this.el = el;                   // Image HTML element
        this.src = src;                 / SRC/pictures
        this.options = options;
        this.status = {loaded: false};    // Record the loading status of images
        this.elRender = elRender;
    }
    // Check whether it is in the loadable area
    checkInView(){
        let bcr = this.el.getBoundingClientRect();
        return bcr.top < window.innerHeight*(this.options.preload || 1.3);
    }
    // Load the image
    load(){
        this.elRender(this.'loading');
        loadAsyncImg(this.src, () = > {
            this.elRender(this.'finish');
        }, () = > {
            this.elRender(this.'error');
        })
        this.status.loaded = true; }}Copy the code

Scroll event

Gets the element’s nearest scrollable parent and adds a scroll event listener to it. While scrolling, load the image if the element is in the viewable area and has not been loaded. Use anti-shake optimization when listening for scrolling events.

const getScrollParent = (el) = > {
    let parentNode = el.parentNode;
    while(parentNode){
        if(/(scroll)|(auto)/.test(getComputedStyle(parentNode)['overflow'])){
            return parentNode;
        }
        parentNode = parentNode.parentNode;
    }
    return parentNode;
}
Copy the code
scrollHandler(e){
    // Determine if each image is visible while scrolling
    this.queueListener.forEach(listener= > {
        let catIn = listener.checkInView();
        catIn && !listener.status.loaded && listener.load();
    })
}
Copy the code
parentNode.addEventListener('scroll', debounce(this.scrollHandler.bind(this), 100));
Copy the code
/ / image stabilization
function debounce(func, wait=0) {    
    if (typeoffunc ! = ='function') {
        throw new TypeError('need a function arguments')}let timeid = null;
    let result;

    return function() {
        let context = this;
        let args = arguments;

        if (timeid) {
            clearTimeout(timeid);
        }
        timeid = setTimeout(function() {
            result = func.apply(context, args);
        }, wait);

        returnresult; }}Copy the code

Image loading rendering

The core of loading an image is to create an image object. If the image is loaded successfully, the image will be displayed. If the image fails to load, the image will be displayed.

class ReactiveListener{
    constructor({el, src, options, elRender}){
        this.el = el;                   // Image HTML element
        this.src = src;                 / SRC/pictures
        this.options = options;
        this.status = {loaded: false};    // Record the loading status of images
        this.elRender = elRender;
    }
    // Load the image
    load(){
        this.elRender(this.'loading');
        loadAsyncImg(this.src, () = > {
            this.elRender(this.'finish');
        }, () = > {
            this.elRender(this.'error');
        })
        this.status.loaded = true; }}Copy the code
function loadAsyncImg(src, resolve, reject){
    let img = new Image();
    img.src = src;
    img.onload = resolve;
    img.onerror = reject;
}
Copy the code
elRender(listener, status){
    let el = listener.el;
    let src = ' ';
    switch(status){
        case 'loading':
            src = this.options.loading || ' ';
            break;
        case 'error':
            src = this.options.error || ' ';
            break;
        default:
            src = listener.src;
            break;
    }
    el.setAttribute('src', src);
}
Copy the code

The complete code

<ol class="box">
    <li v-for="img in imglist" :key="img.id">
        <img v-lazy="img.src" alt="">
    </li>
</ol>
Copy the code
const loading = "./image/loading.gif";
const error = './image/error.png';
Vue.use(VueLazyLoad, {
    preload: 1.3,
    loading,
    error,
})
Copy the code
const getScrollParent = (el) = > {
    let parentNode = el.parentNode;
    while(parentNode){
        if(/(scroll)|(auto)/.test(getComputedStyle(parentNode)['overflow'])){
            return parentNode;
        }
        parentNode = parentNode.parentNode;
    }
    return parentNode;
}
function loadAsyncImg(src, resolve, reject){
    let img = new Image();
    img.src = src;
    img.onload = resolve;
    img.onerror = reject;
}

/ / image stabilization
function debounce(func, wait=0) {    
    if (typeoffunc ! = ='function') {
        throw new TypeError('need a function arguments')}let timeid = null;
    let result;

    return function() {
        let context = this;
        let args = arguments;

        if (timeid) {
            clearTimeout(timeid);
        }
        timeid = setTimeout(function() {
            result = func.apply(context, args);
        }, wait);

        returnresult; }}const Lazy = (Vue) = > {
    // Treat each image as an object
    class ReactiveListener{
        constructor({el, src, options, elRender}){
            this.el = el;                   // Image HTML element
            this.src = src;                 / SRC/pictures
            this.options = options;
            this.status = {loaded: false};    // Record the loading status of images
            this.elRender = elRender;
        }
        // Check whether it is in the loadable area
        checkInView(){
            let bcr = this.el.getBoundingClientRect();
            return bcr.top < window.innerHeight*(this.options.preload || 1.3);
        }
        // Load the image
        load(){
            this.elRender(this.'loading');
            loadAsyncImg(this.src, () = > {
                this.elRender(this.'finish');
            }, () = > {
                this.elRender(this.'error');
            })
            this.status.loaded = true; }}// Encapsulates lazy-loaded logic
    return class LazyClass{
        constructor(options){
            this.options = options;
            this.hasAddScrollListener = false;
            this.queueListener = [];
        }

        scrollHandler(e){
            console.log('scroll')
            // Determine if each image is visible while scrolling
            this.queueListener.forEach(listener= > {
                let catIn = listener.checkInView();
                catIn && !listener.status.loaded && listener.load();
            })
        }

        add(el, bindings, vnode, oldVnode){
            // Find the parent element, listen for the parent element to scroll events, custom instruction bind does not get el, so use nextTick
            Vue.nextTick(() = > {
                const parentNode = getScrollParent(el);
                if(parentNode && !this.hasAddScrollListener){
                    this.hasAddScrollListener = true;
                    parentNode.addEventListener('scroll', debounce(this.scrollHandler.bind(this), 100));
                }
                // Add each image to the queue
                const listener = new ReactiveListener({
                    el: el,
                    src: bindings.value,
                    options: this.options,
                    elRender: this.elRender,
                });
                this.queueListener.push(listener);
                // Determine if the image is visible for the first time
                this.scrollHandler()
            })
        }

        elRender(listener, status){
            let el = listener.el;
            let src = ' ';
            switch(status){
                case 'loading':
                    src = this.options.loading || ' ';
                    break;
                case 'error':
                    src = this.options.error || ' ';
                    break;
                default:
                    src = listener.src;
                    break;
            }
            el.setAttribute('src', src); }}}const VueLazyLoad = {
    install(Vue, options){
        // Encapsulate lazy logic in a class
        const lazyClass = Lazy(Vue);
        const lazy = new lazyClass(options);
        Vue.directive('lazy', {
            bind: lazy.add.bind(lazy),
        })
    }
}
Copy the code

The effect