Site performance is not good, all look at the picture less!

The number of pictures directly affects the speed of webpage loading, especially when your mobile phone network is not good (manual @ Apple) a picture turn turn turn, turn for a long time also can not turn out, gas directly want to smash the phone…

So in daily development we don’t load all the images at once, we load a few images first to make the user feel that the page has been loaded. In fact, we quietly load other images in the background, and the image is loaded without anyone knowing it, and it has a very smooth experience.

Open Source Component library

ElementUI, Vant, Antdv, ElementUI, Vant, Antdv, ElementUI, Vant, Antdv Vant uses a third-party plugin called VUe-LazyLoad.

React also has many excellent component libraries. If you are interested, you can check them out for yourself

In ElementUI, it is easy to implement. In loading state, it is a div, which will be replaced by an IMG tag when the image is rendered. However, vue-LazyLoad, as a plug-in, is more considerate and has more comprehensive functions. It is implemented by providing a custom global directive, V-lazy. But their core principle is the same, that is, by adding a scroll event listener to the container with scroll properties, only render images in the visible area, so as to achieve lazy loading of images.

The preparatory work

Finished looking at other people’s functions we come to their own implementation of a, here I am referring to vue-Lazyload source code as far as possible to achieve a simple plug-in.

Let’s start with a few images, so I’m going to use ElementUI images directly here, and put them in the list for later display

Simple HTML template

<div id="app">
  <div class="box">
    <li v-for="(img, index) in imgs" :key="index">
      <img v-lazy="img" alt="" />
    </li>
  </div>
</div>
Copy the code

OK, that’s all we need to do, so let’s get straight to our business.

The install method

Plug-ins developing Vue must provide an Install method that takes the Vue constructor as its first argument and an optional option object as its second argument. We then use it with vue. use, where the install method is only installed once when called multiple times by the same plug-in, and Vue does an internal cache optimization to avoid repeated installs.

Borrow an example from the official website:

MyPlugin.install = function (Vue, options) {
  // 1. Add a global method or property
  Vue.myGlobalMethod = function () {
    / / logic...
  }

  // 2. Add global resources
  Vue.directive('my-directive', {
    bind (el, binding, vnode, oldVnode) {
      / / logic...}... })// 3. Inject component options
  Vue.mixin({
    created: function () {
      / / logic...}... })// 4. Add instance methods
  Vue.prototype.$myMethod = function (methodOptions) {
    / / logic...}}Copy the code

When we call vue.use (MyPlugin), the install method is automatically executed within the Vue and automatically merged into the component instance according to the internal merge strategy, so that we can use the functionality provided by the plug-in directly.

That said how to develop plug-ins, now start to implement a plug-in of our own.

Let’s call the plugin VueLazyload and export it:

const VueLazyload = {
  install: function() {
		// ...}}export default VueLazyload
Copy the code

All we need to do in the install method is process the options passed in by the user and add a global directive v-lazy to the Vue instance

install: function (Vue) {
  // Save the options passed in by the user
  const options = arguments.length > 1 && arguments[1]! = =undefined ? arguments[1] : {}

  // Function currization encapsulates each function into a class. If the function needs to be extended, a new class can be added directly
  const LazyClass = Lazy(Vue)
  const lazy = new LazyClass(options)

  Vue.directive('lazy', {
    bind: lazy.add.bind(lazy), // Bind this to lazy})}Copy the code

The first argument to the install method is the Vue constructor, which is passed by default when vue.use () is used so that the plug-in is not strongly associated with the Vue. Assuming that the Vue constructor is not passed in, the use of the Vue inside the plug-in must be explicitly introduced, which can lead to unexpected exceptions when the version is inconsistent with the one the user is using.

Options can be retrieved directly from arguments. The key is to generate an instance of lazy, which I didn’t understand until I read the source code for a long time. Using the function currification to pass Vue as a parameter to a function that specifically generates a class, we can wrap each function in a class and then return it through the lazy function, Vue-lazyload function is very full, interested can go to understand his source code.

The next step is to customize a global directive, using vue.directive (). If you are not familiar with it, you can go to the documentation and use an initial hook function called bind to bind the directive to the element. We then assign the add method on the lazy instance to the hook function. Note the pointer to this, which needs to be explicitly specified as lazy using the bind method

Lazy approach

The first thing to understand is that this method is used to return a class and then generate an instance from that class:

const Lazy = Vue= > {
  return class LazyClass {
    constructor() {
    
    }
    add() {

    }
  }
}
Copy the code

Now that our basic framework is complete, the next thing we need to do is handle custom instructions.

The add method is used to bind directives to img elements. Its parameters are el, Bindings, and vNode parameters. Here we deal with the core principle of lazy loading.

  • Find the image’s parent container with scroll, and add listener events to it
  • Determines if the element is in the container’s viewable area, if not, it doesn’t need to be loaded

According to this principle, we implement it step by step.

Adding listening Events

Whereas to find the parent of the instruction-bound element, you must wait until the element is inserted into the parent, one way is to use the instruction’s inserted hook function directly, but using bind here doesn’t seem to get the parent element. Don’t be too quick. DOM updates in Vue are asynchronous queues, so we can get the updated DOM via vue.nexttick (), which means we need to write the logic to the nextTick callback.

Let’s write a method to get the parent element of the bound element with the scroll attribute:

const getScrollParent = el= > {
  let parent = el.parentNode
  while (parent) {
    // Check whether the parent element is scrollable
    if (/scroll|auto/.test(getComputedStyle(parent)['overflow']) {return parent
    }
    parent = parent.parentNode
  }
  return window
}
Copy the code

This is simply done by traversing the overflow property of the parent element and returning scroll or auto.

Once we get the parent element, we determine if it is bound to a scroll event, and if not, we bind it to a scroll event.

let scrollParent = getScrollParent(el)
// If the parent element exists and the 'scroll' event is not bound
if (scrollParent && !this.bindHandle) {
  this.bindHandle = true
  scrollParent.addEventListener('scroll', throttle(this.handleLazyLoad.bind(this), 200))}Copy the code

Rolling events are usually optimized by adding a function to throttle, which you can write yourself or use throttle-debounce or the _. Throttle provided by Lodash.

Remember to destroy events

First apply colours to a drawing

The next step is to determine if the element is in the visible area of the container. Here we define a class that generates each instance of the monitored image. Since we used the lazy method to generate the lazy instance, we can add a class directly to the lazy method:

// Each image element is constructed as an instance of a class for easy extension
class ReactiveListener {
  constructor({ el, src, options, elRender }) {
    this.el = el
    this.src = src
    this.options = options
    this.state = {
      loading: false.// Whether it has been loaded
      error: false.// Is there an error
    }
    this.elRender = elRender
  }
  // Check whether the current image is in the but area
  checkInView() {
    // ...
  }
  // Load the image
  load() {
    // ...}}Copy the code

As we said earlier, if the image is in the viewable area, it needs to be loaded, so both methods are necessary later.

Back in the add method, after adding the scroll event, we need to push the bound image element to the queue, traverse the queue, and call the image load method to load it if it is not loaded and in the visible area. Here is the complete code for both methods:

add(el, bindings, vnode) {
  Vue.nextTick((a)= > {
    let scrollParent = getScrollParent(el)
    if (scrollParent && !this.bindHandle) {
      this.bindHandle = true
      scrollParent.addEventListener('scroll', throttle(this.handleLazyLoad.bind(this), 2z00))
    }
    // Determine if the element is in the visual area of the container. If not, no rendering is required
    const listener = new ReactiveListener({
      el,
      src: bindings.value,
      options: this.options,
      elRender: this.elRender.bind(this})),// Place each image instance in the array
    this.listenerQueue.push(listener)
    this.handleLazyLoad()
  })
}
handleLazyLoad() {
  // Iterate over all images, if not already loaded and in the viewable area
  this.listenerQueue.forEach(listener= > {
    if(! listener.state.loading) {let catIn = listener.checkInView()
      catIn && listener.load()
    }
  })
}
Copy the code

It’s also easy to detect and load images, so I’ll just write:

// Check whether the current image is in the but area
checkInView() {
  let { top } = this.el.getBoundingClientRect()
  // Relative to the window height
  return top < window.innerHeight * (this.options.preload || 1.3)}// Load the image
load() {
  // Load the loading image first
  // If the image is loaded successfully, it will be replaced with a normal image
  this.elRender(this.'loading')
  console.log(this.src)
  loadImageAsync(
    this.src,
    () => {
      setTimeout((a)= > {
        this.state.loading = true
        this.elRender(this.'finish')},1000}, () => {this.state.error = true
      this.elRender(this.'error')})}Copy the code

Note that getBoundingClientRect retrieves the size and position of the element relative to the viewport. A new API called Intersection Observer is used in Vue-lazyLoad. This API allows you to listen for the visibility of elements with asynchronous callbacks. If you are interested, go directly to MDN

The loadImageAsync method is to create an Image object. If the Image is successfully loaded, the first callback will be generated. If the Image fails to be loaded, the second callback will be generated. You can also turn this method into a Promise that actually loads images asynchronously.

const loadImageAsync = (src, resolve, reject) = > {
  let image = new Image()
  image.src = src
  image.onload = resolve
  image.onerror = reject
}
Copy the code

elRender

Ok, that’s basically the core code, and finally we need a method to change the SRC path of the image depending on the state. This method is the elRender method on the lazy instance:

// Render method
elRender(listener, state) {
  let el = listener.el
  let src = ' '
  switch (state) {
    case 'loading':
      src = listener.options.loading || ' ' // Loading that the user uploaded
      break
    case 'error':
      src = listener.options.error || ' ' // Error passed in by the user
      break
    default:
      src = listener.src // finish
      break
  }
  el.setAttribute('src', src)
}
Copy the code

If the image is loading, the loading diagram is displayed. If the loading fails, the error diagram is displayed. If the loading succeeds, the original image is displayed.

rendering

Finally, let’s use our own image lazy loading plugin:

Vue.use(VueLazyLoad, {
  preload: 1.loading: 'http://images.h-ui.net/icon/detail/loading/075.gif',})Copy the code

Here I set the default to load a screen and passed in the loading diagram

As you can see from the image, the first four images have been rendered. The fifth image has no SRC address, which proves that we have implemented lazy loading.

This is another story

Of course, the implementation of the source code has a lot of details worth our learning, here is just a simple with you to achieve a lazy loading plug-in picture, there are many cases are not considered, I still recommend you to take a look at vue-Lazyload source code.

Recently, I just joined a new company, and I have been working on the project of the company, so I don’t have much time to read other things. Besides, the project of the company uses antDV component library, so I read the source code of ANTDV more, so I don’t have any spare time to write the source code article of ElementUI. But I’ll stick with the ElementUI series if I have time, because the source code looks more comfortable than ANTDV.

The source code

Finally, attach the source code, don’t forget to send a wave of attention and like ~

/** * function throttling @param {*} action Callback * @param {*} delay Delay */
function throttle(action, delay) {
  var timeout = null
  var lastRun = 0
  return function () {
    if (timeout) {
      return
    }
    var elapsed = Date.now() - lastRun
    var context = this
    var args = arguments
    var runCallback = function runCallback() {
      lastRun = Date.now()
      timeout = false
      action.apply(context, args)
    }
    if (elapsed >= delay) {
      runCallback()
    } else {
      timeout = setTimeout(runCallback, delay)
    }
  }
}

// Gets the element's parent element with scroll
const getScrollParent = el= > {
  let parent = el.parentNode
  while (parent) {
    // Check whether the parent element is scrollable
    if (/scroll|auto/.test(getComputedStyle(parent)['overflow']) {return parent
    }
    parent = parent.parentNode
  }
  return window
}

const loadImageAsync = (src, resolve, reject) = > {
  let image = new Image()
  image.src = src
  image.onload = resolve
  image.onerror = reject
}

const Lazy = Vue= > {
  // Each image element is constructed as an instance of a class for easy extension
  class ReactiveListener {
    constructor({ el, src, options, elRender }) {
      this.el = el
      this.src = src
      this.options = options
      this.state = {
        loading: false.// Whether it has been loaded
        error: false.// Is there an error
      }
      this.elRender = elRender
    }
    // Check whether the current image is in the but area
    checkInView() {
      let { top } = this.el.getBoundingClientRect()
      // Relative to the window height
      return top < window.innerHeight * (this.options.preload || 1.3)}// Load the image
    load() {
      // Load the loading image first
      // If the image is loaded successfully, it will be replaced with a normal image
      this.elRender(this.'loading')
      console.log(this.src)
      loadImageAsync(
        this.src,
        () => {
          setTimeout((a)= > {
            this.state.loading = true
            this.elRender(this.'finish')},1000}, () => {this.state.error = true
          this.elRender(this.'error')})}}class LazyClass {
    constructor(options) {
      this.options = options
      this.bindHandle = false // Whether the Scroll event is bound
      this.listenerQueue = []
    }

    add(el, bindings, vnode) {
      Vue.nextTick((a)= > {
        let scrollParent = getScrollParent(el)
        if (scrollParent && !this.bindHandle) {
          this.bindHandle = true
          scrollParent.addEventListener('scroll', throttle(this.handleLazyLoad.bind(this), 200))}// Determine if the element is in the visual area of the container. If not, no rendering is required
        const listener = new ReactiveListener({
          el,
          src: bindings.value,
          options: this.options,
          elRender: this.elRender.bind(this})),// Place each image instance in the array
        this.listenerQueue.push(listener)
        this.handleLazyLoad()
      })
    }
    handleLazyLoad() {
      // Iterate over all images, if not already loaded and in the viewable area
      this.listenerQueue.forEach(listener= > {
        if(! listener.state.loading) {let catIn = listener.checkInView()
          catIn && listener.load()
        }
      })
    }
    // Render method
    elRender(listener, state) {
      let el = listener.el
      let src = ' '
      switch (state) {
        case 'loading':
          src = listener.options.loading || ' ' // Loading that the user uploaded
          break
        case 'error':
          src = listener.options.error || ' ' // Error passed in by the user
          break
        default:
          src = listener.src
          break
      }
      el.setAttribute('src', src)
    }
  }
  
  return LazyClass
}

const VueLazyLoad = {
  install: function (Vue) {
    // Save the options passed in by the user
    const options = arguments.length > 1 && arguments[1]! = =undefined ? arguments[1] : {}

    // Function currization encapsulates each function into a class. If the function needs to be extended, a new class can be added directly
    const LazyClass = Lazy(Vue)
    const lazy = new LazyClass(options)

    Vue.directive('lazy', {
      bind: lazy.add.bind(lazy), // Bind this to lazy}})},export default VueLazyLoad
Copy the code