SoFunction
Updated on 2025-03-10

How to write a custom command for Vue3

Front-end peak
The following article comes from the front-end peak of WeChat public account

background

As we all know, the core idea 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. In this process, we do not need to manually operate the DOM.

However, in some scenarios, we still cannot avoid operating the DOM. Since the framework takes over the creation and updating of DOM elements, it can inject user code within the life cycle of the DOM element, soCustom instructions are designed and provided to allow users to perform some underlying DOM operations.

To give a practical example - lazy loading of pictures. Lazy image loading is a common way of performance optimization. Since it only loads pictures in the visual area, it can reduce many unnecessary requests and greatly improve the user experience.

The implementation principle of lazy image loading is also very simple. When the image does not enter the visual area, we only need to let the src attribute of the img tag point to a default image. After it enters the visual area, replace its src pointing to the real image address.

If we want to implement lazy image loading in our project, then using custom instructions is the most appropriate. Then let me take you to use Vue3 to implement a lazy image loading custom instruction v-lazy.

Plugin

In order to make this directive convenient for multiple projects, we make it a plug-in:

const lazyPlugin = {
  install (app, options) {
    ('lazy', {
      // Directive object    })
  }
}

export default lazyPlugin

Then refer to it in the project:

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

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

Usually a Vue3 plugin will expose the install function, and when the app instance uses the plugin, the function will be executed. Inside the install function, register a global directive so that they can be used in the component.

Implementation of instructions

The next thing we have to do is implement the instruction object. One instruction definition object can provide multiple hook functions, such asmountedupdatedunmounted etc. We can write corresponding code in the appropriate hook function to achieve the requirements.

Before writing code, we might as well think about several key steps to implement lazy image loading.

Image management:

Manage the DOM of the image, real src, preloaded url, loading status, and loading of the image.

Visibility area judgment:

Determine whether the picture enters the visual area.

Regarding image management, we designed the ImageManager class:

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

export class ImageManager {
  constructor(options) {
     = 
     = 
     = 
     = 
     = 
    
    ()
  }
  render() {
    ('src', src)
  }
  load(next) {
    if ( > ) {
      return
    }
    (next)
  }
  renderSrc(next) {
    loadImage().then(() => {
       = 
      ()
      next && next()
    }).catch((e) => {
       = 
      ()
      (`load failed with src image(${}) and the error msg is ${}`)
      next && next()
    })
  }
}

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

     = function () {
      resolve()
      dispose()
    }

     = function (e) {
      reject(e)
      dispose()
    }

     = src

    function dispose () {
       =  = null
    }
  })
}

First of all, for the picture, it has three states, loading, loading completion, and loading failure.

whenImageManager When instantiating, in addition to initializing some data, it will also execute the loading image of the src of the corresponding img tag.loading, this is equivalent to the default loading image.

When executedImageManager When the object's load method is used, the status of the image will be judged. If it is still loading, it will load its real src. This is used here.loadImage Image preloading technology implements requesting src images, and after success, replace src of the img tag and modify the status, so as to complete the loading of the real address of the image.

With the picture manager, we need to determine the visual area and manage the manager of multiple pictures, and design the Lazy class:

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

export default class Lazy {
  constructor(options) {
     = []
    ()
    
     =  || DEFAULT_URL
     =  || DEFAULT_URL
  }
  add(el, binding) {
    const src = 
    
    const manager = new ImageManager({
      el,
      src,
      loading: ,
      error: 
    })
    
    (manager)
    
    (el)
  }
  initIntersectionObserver() {
     = new IntersectionObserver((entries) => {
      ((entry) => {
        if () {
          const manager = ((manager) => {
            return  === 
          })
          if (manager) {
            if ( === ) {
              (manager)
              return
            }
            ()
          }
        }
      })
    }, {
      rootMargin: '0px',
      threshold: 0
    })
  }
  removeManager(manager) {
    const index = (manager)
    if (index > -1) {
      (index, 1)
    }
    if () {
      ()
    }
  }
}

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

    ('lazy', {
      mounted: (lazy)
    })
  }
}

In this way, whenever the image element is bound to the v-lazy directive, and inmounted When the hook function is executed, the add method of the Lazy object will be executed. The first parameter el corresponds to the DOM element object corresponding to the image, and the second parameterbindingIt is the value bound to the instruction object, such as:

<img class="avatar" v-lazy="">

The corresponding value is the command bound value, so You can get the real address of the picture.

With the DOM element object of the image and the real image address, you can create an image manager object based on them and add it tomanagerQueue , and the visual area of ​​the DOM element of the picture is also observed.

For the judgment of the picture entering the visual area, the main useIntersectionObserver API,Its corresponding callback function parameter entries isIntersectionObserverEntry Object array. When the visible proportion of the observed element exceeds the specified threshold, the callback function will be executed, traversed the entries, got each entry, and then judgedWhether it is true, if so, it means that the DOM element corresponding to the entry object has entered the viewing area.

Then, based on the comparison of the DOM elementsmanagerQueue Find the corresponding one inmanager, and determine the loading status of the corresponding picture.

If the image is loading, then it is executed Functions to complete the loading of the real image; if it is loaded, it will be directly frommanagerQueue Remove its corresponding manager in , and stop observing the image DOM element.

At present, we have implemented a series of processing of delayed loading after image elements are mounted to the page. However, when the element is uninstalled from the page, some cleaning operations are also required:

export default class Lazy {
  remove(el) {
    const manager = ((manager) => {
      return  === el
    })
    if (manager) {
      (manager)
    }
  }
}

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

    ('lazy', {
      mounted: (lazy),
      remove: (lazy)
    })
  }
}

When an element is uninstalled, its corresponding image manager will also be frommanagerQueue is removed and observation of the image DOM element is stopped.

In addition, if the value bound by the v-lazy instruction is dynamically modified, that is, the request address of the real image, then the corresponding modification should be made inside the instruction:

export default class ImageManager {
  update (src) {
    const currentSrc = 
    if (src !== currentSrc) {
       = src
       = 
    }
  }  
}

export default class Lazy {
  update (el, binding) {
    const src = 
    const manager = ((manager) => {
      return  === el
    })
    if (manager) {
      (src)
    }
  }    
}

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

    ('lazy', {
      mounted: (lazy),
      remove: (lazy),
      update: (lazy)
    })
  }
}

So far, we have implemented a simple image lazy loading instruction. Can we still do some optimizations based on this?

Optimization of instructions
In the process of loading the real url of the image, we usedloadImageTo preload the image, then obviously for multiple images with the same url, preloading only needs to be done once.

To achieve the above requirements, we can create a cache cache inside the Lazy module:

export default class Lazy {
  constructor(options) {
    // ...
     = new Set()
  }
}

Then createImageManager When the instance isPass the cache in:

const manager = new ImageManager({
  el,
  src,
  loading: ,
  error: ,
  cache: 
})

Then make the following modifications to the ImageManager:

export default class ImageManager {
  load(next) {
    if ( > ) {
      return
    }
    if (()) {
       = 
      ()
      return
    }
    (next)
  }
  renderSrc(next) {
    loadImage().then(() => {
       = 
      ()
      next && next()
    }).catch((e) => {
       = 
      ()
      ()
      (`load failed with src image(${}) and the error msg is ${}`)
      next && next()
    })  
  }
}

In each executionload Before determining whether it already exists from the cache, then executing itloadImage Update the cache after the preloaded image is successful.

Through this method of changing space and time, some duplicate url requests are avoided, achieving the purpose of optimizing performance.

Summarize:

The complete instruction implementation of lazy loading image instructions can be viewed in vue3-lazy, and it is also applied in my course "Vue3 Developing High-Quality Music Web App".

The core of lazy loading image instructions is the applicationIntersectionObserver API To determine whether the image enters the viewing area, this feature is supported in modern browsers, but IE browser does not. At this time, you can scroll some events of the parent element such as scroll, resize, etc. by listening to the image, and then use some DOM calculations to determine whether the image element enters the viewing area. However, Vue3 has clearly no longer supported IE, so it is enough to just use the IntersectionObserver API.

In addition to the hook functions used in lazy loading image custom instructions, Vue3's custom instructions also provide some other hook functions. When you develop custom instructions in the future, you can check its documents and write corresponding code logic in suitable hook functions.

Related links:

[1] IntersectionObserver: /zh-CN/docs/Web/API/IntersectionObserver

[2] vue3-lazy: /ustbhuangyi/vue3-lazy

[3] "Vue3 Develops High-Quality Music Web App":/class/

[4] Vue3 custom directive documentation: /guide/