background

As we all know, the core idea of Vue.js is data-driven + componentization. Usually, the process of developing a page is to write some components and drive the re-rendering of components by modifying the data. There is no need to manually manipulate the DOM in this process.

In some cases, however, DOM manipulation is unavoidable. Since the vue.js framework takes over the creation and updating of DOM elements, it can inject user code during the life cycle of DOM elements, so vue.js designs and provides custom instructions that allow users to perform some low-level DOM operations.

Take a practical example — lazy loading of images. Image lazy loading is a common way to optimize performance. Since it only loads images in the visible area, it can reduce many unnecessary requests and greatly improve the user experience.

The implementation principle of image lazy loading is also very simple. When the image is not in the viewable area, we just need to make the SRC attribute of the IMG tag point to the default image. After it is in the viewable area, we can replace its SRC to point to the real image address.

If we want to implement lazy image loading in vue. js project, then using a custom directive is most suitable, so let me show you how to use Vue3 to implement a custom directive v-lazy image loading.

The plug-in

To make this directive easy to use for multiple projects, we make it a plug-in:

const lazyPlugin = {
  install (app, options) {
    app.directive('lazy', {
      // The instruction object}}})export default lazyPlugin
Copy the code

Then reference it in your project:

import { createApp } from 'vue'
import App from './App.vue'
import lazyPlugin from 'vue3-lazy'

createApp(App).use(lazyPlugin, {
  // Add some configuration parameters
})
Copy the code

Usually a Vue3 plug-in exposes the install function, which is executed when the app instance uses the plug-in. Inside the install function, register a global directive with app.directive so that it can be used in components.

Implementation of instructions

A directive definition object can provide multiple hook functions, such as Mounted, updated, and unmounted.

Before writing the code, let’s think about a few key steps for implementing lazy image loading.

  • Picture management

Manage image DOM, true SRC, preloaded url, loaded state, and image loading.

  • Judgment of visual area

Determine whether the picture has entered the viewable area.

For image management, we designed the ImageManager class:

const State = {
  loading: 0.loaded: 1.error: 2
}

export class ImageManager {
  constructor(options) {
    this.el = options.el
    this.src = options.src
    this.state = State.loading
    this.loading = options.loading
    this.error = options.error
    
    this.render(this.loading)
  }
  render() {
    this.el.setAttribute('src', src)
  }
  load(next) {
    if (this.state > State.loading) {
      return
    }
    this.renderSrc(next)
  }
  renderSrc(next) {
    loadImage(this.src).then(() = > {
      this.state = State.loaded
      this.render(this.src)
      next && next()
    }).catch((e) = > {
      this.state = State.error
      this.render(this.error)
      console.warn(`load failed with src image(The ${this.src}) and the error msg is ${e.message}`)
      next && next()
    })
  }
}

export default function loadImage (src) {
  return new Promise((resolve, reject) = > {
    const image = new Image()

    image.onload = function () {
      resolve()
      dispose()
    }

    image.onerror = function (e) {
      reject(e)
      dispose()
    }

    image.src = src

    function dispose () {
      image.onload = image.onerror = null}})}Copy the code

First, for an image, there are three states: loading, loading complete, and loading failed.

When ImageManager instantiates, in addition to initializing some data, it will also load the image loading of its corresponding IMG tag SRC, which is equivalent to the default loaded image.

When executing the load method of ImageManager object, it will judge the state of the image. If the image is still being loaded, it will load its real SRC. Here, loadImage image preloading technology is used to request the SRC image, and then replace the SRC tag after success. And modify the state, so complete the loading of the real address of the image.

With the image manager, we need to implement visual area judgment and manage multiple image managers, and design Lazy classes:

const DEFAULT_URL = 'data:image/gif; base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7'

export default class Lazy {
  constructor(options) {
    this.managerQueue = []
    this.initIntersectionObserver()
    
    this.loading = options.loading || DEFAULT_URL
    this.error = options.error || DEFAULT_URL
  }
  add(el, binding) {
    const src = binding.value
    
    const manager = new ImageManager({
      el,
      src,
      loading: this.loading,
      error: this.error
    })
    
    this.managerQueue.push(manager)
    
    this.observer.observe(el)
  }
  initIntersectionObserver() {
    this.observer = new IntersectionObserver((entries) = > {
      entries.forEach((entry) = > {
        if (entry.isIntersecting) {
          const manager = this.managerQueue.find((manager) = > {
            return manager.el === entry.target
          })
          if (manager) {
            if (manager.state === State.loaded) {
              this.removeManager(manager)
              return
            }
            manager.load()
          }
        }
      })
    }, {
      rootMargin: '0px'.threshold: 0})}removeManager(manager) {
    const index = this.managerQueue.indexOf(manager)
    if (index > -1) {
      this.managerQueue.splice(index, 1)}if (this.observer) {
      this.observer.unobserve(manager.el)
    }
  }
}

const lazyPlugin = {
  install (app, options) {
    const lazy = new Lazy(options)

    app.directive('lazy', {
      mounted: lazy.add.bind(lazy)
    })
  }
}
Copy the code

When the mounted hook is executed, the lazy object is added to the image element. The first el parameter is the DOM element of the image element, and the second binding parameter is the value of the binding. Such as:

<img class="avatar" v-lazy="item.pic">
Copy the code

Where item. PIC corresponds to the value of the instruction binding, so the real address of the image can be obtained through binding.value.

Once you have the image’s DOM element object and the actual image address, you can create a picture manager object from them, add it to the managerQueue, and view the image’S DOM element in the viewable area.

For the image into the judgement of visual area, main use of IntersectionObserver API, its corresponding callback function parameter entries, is IntersectionObserverEntry array of objects. When the visible proportion of observed elements exceeds the specified threshold, the callback function will be executed to traverse the entries, get each entry, and determine whether entry.isIntersecting is true. If so, the DOM element corresponding to the Entry object is visible.

We then find the corresponding manager from the managerQueue based on the DOM element alignment and determine its image loading status.

If the image is in the loading state, then the manager.load function is executed to complete the loading of the real image. If it is loaded, remove the corresponding manager directly from the managerQueue and stop viewing the image DOM element.

At present, we have implemented a series of processing of delayed loading of image elements after they are mounted to the page. However, there are also some cleanup operations that need to be performed when the element is unloaded from the page:

export default class Lazy {
  remove(el) {
    const manager = this.managerQueue.find((manager) = > {
      return manager.el === el
    })
    if (manager) {
      this.removeManager(manager)
    }
  }
}

const lazyPlugin = {
  install (app, options) {
    const lazy = new Lazy(options)

    app.directive('lazy', {
      mounted: lazy.add.bind(lazy),
      remove: lazy.remove.bind(lazy)
    })
  }
}
Copy the code

When an element is uninstalled, its image manager is removed from the managerQueue and the image DOM element is not observed.

In addition, if the value of the V-lazy directive binding is dynamically changed, that is, the requested address of the real image, then the corresponding change should be made inside the directive:

export default class ImageManager {
  update (src) {
    const currentSrc = this.src
    if(src ! == currentSrc) {this.src = src
      this.state = State.loading
    }
  }  
}

export default class Lazy {
  update (el, binding) {
    const src = binding.value
    const manager = this.managerQueue.find((manager) = > {
      return manager.el === el
    })
    if (manager) {
      manager.update(src)
    }
  }    
}

const lazyPlugin = {
  install (app, options) {
    const lazy = new Lazy(options)

    app.directive('lazy', {
      mounted: lazy.add.bind(lazy),
      remove: lazy.remove.bind(lazy),
      update: lazy.update.bind(lazy)
    })
  }
}
Copy the code

So far, we have implemented a simple image lazy loading command, on this basis, can we do some optimization?

Optimization of instructions

In the loading process of the real URL of the image, we used loadImage to do the image preloading, so obviously for multiple images of the same URL, the preloading only needs to be done once.

To implement this, we can create a cache inside the Lazy module:

export default class Lazy {
  constructor(options) {
    // ...
    this.cache = new Set()}}Copy the code

The cache is then passed in when the ImageManager instance is created:

const manager = new ImageManager({
  el,
  src,
  loading: this.loading,
  error: this.error,
  cache: this.cache
})
Copy the code

Then make the following changes to ImageManager:

export default class ImageManager {
  load(next) {
    if (this.state > State.loading) {
      return
    }
    if (this.cache.has(this.src)) {
      this.state = State.loaded
      this.render(this.src)
      return
    }
    this.renderSrc(next)
  }
  renderSrc(next) {
    loadImage(this.src).then(() = > {
      this.state = State.loaded
      this.render(this.src)
      next && next()
    }).catch((e) = > {
      this.state = State.error
      this.cache.add(this.src)
      this.render(this.error)
      console.warn(`load failed with src image(The ${this.src}) and the error msg is ${e.message}`)
      next && next()
    })  
  }
}
Copy the code

Determine whether the image exists in the cache before each load, and then update the cache after the loadImage preload image succeeds.

Through this means of space for time, some repeated URL requests are avoided and performance is optimized.

conclusion

The full implementation of the lazy load Image directive can be viewed in VUe3 – Lazy, which is also used in my course “Vue3 Developing a Quality Music Web App”.

The core of the lazy image loading command is that IntersectionObserver API is applied to judge whether an image enters the viewable area. This feature is supported in modern browsers but not in Internet Explorer. In this case, events such as Scroll and resize of the parent element of the image can be monitored. It then performs some DOM calculations to determine if the image element is in view. However, Vue3 explicitly no longer supports IE, so simply using IntersectionObserver API is sufficient.

In addition to the hook function used in lazy loading image custom instruction, Vue3 custom instruction also provides some other hook functions, you can refer to its documentation when developing custom instruction in the future, and write the corresponding code logic in the appropriate hook function.