Custom image lazy loading instruction v-lazy in Vue2

Custom image lazy loading instruction v-lazy in Vue2

Since I want to optimize the response speed of the website when I am developing the front page of my personal blog, I want to achieve the effect of lazy loading of pictures.

I realized it through 自定义指令v-lazy, so here I would like to share with you the development process of this command and the solutions to its difficulties.

1. Explanation of the main knowledge involved

The custom image lazy loading instruction mainly involves the following three pieces of knowledge:

  • Custom directives in Vue2
  • Communication between modules using event bus
  • Web API used
    • Element.clientHeight
    • Element.getBoundingClientRect()

Below I will introduce these knowledge points one by one.

1.1 Custom directives in Vue2

Below I will only give a brief introduction to custom commands. For a detailed introduction, you can refer to Vue official website - custom commands .

1.1.1 Hook function of instruction object

  • bind: Called only once, when the directive is bound to the element for the first time. One-time initialization settings can be performed here.
  • inserted: Called when the bound element is inserted into the parent node (only the parent node is guaranteed to exist, but not necessarily inserted into the document).
  • update: Called when the VNode of the component is updated, but it may happen before the update of its child VNode. The value of the directive may or may not have changed. Unnecessary template updates can be ignored by comparing the values ​​before and after the update (see below for detailed hook function parameters).
  • componentUpdated: Called after the VNode of the component where the command is located and its child VNodes are all updated.
  • unbind: Called only once, when the instruction is unbound from the element.

The parameters of the hook function mainly include these four el、binding、vnode、oldVnode.

1.1.2 Hook function parameters

  • el: The element bound to the instruction, which can be used to directly manipulate the DOM.
  • binding: an object containing the following properties:
    • name: The command name, excluding the v- prefix.
    • value: The binding value of the directive, such as: v-my-directive="1 + 1", the binding value is 2.
    • oldValue: The previous value bound by the directive, only available in update and componentUpdated hooks. Available whether or not the value has changed.
    • expression: Instruction expression in string form. For example, in v-my-directive="1 + 1", the expression is "1 + 1".
    • arg: Arguments passed to the command, optional. For example, in v-my-directive:foo, the parameter is "foo".
    • modifiers: An object containing modifiers. For example: in v-my-directive.foo.bar, the modifier object is { foo: true, bar: true }.
  • vnode: The virtual node generated by Vue compilation. Move to VNode API for more details.
  • oldVnode: the previous virtual node, only available in update and componentUpdated hooks.

1.2 Use event bus for communication between modules

Friends who are not familiar with the event bus can refer to this blog What is the Vue event bus (EventBus) .

  • Listen for events on the event bus—call the $on method
  • Trigger an event on the event bus—call the $emit method
  • Unlisten to events on the event bus—call the $off method

We can use the vue example to implement the event bus, or we can encapsulate it ourselves; I used the first method.

So the event bus configuration file— eventBus.js the code is as follows:

import Vue from "vue";
const eventBus = new Vue({
    
    });
/*
 * 事件名:mainScroll
 * 含义:主区域滚动条位置变化后触发
 * 参数:
 * - 滚动的dom元素,如果是undefined,则表示dom元素已经不存在
 */
//在Vue.prototype原型上注册事件总线,方便vue实例对象监听和触发
Vue.prototype.$bus = eventBus;
//导出事件总线,方便在其他js模块监听和触发事件总线上的事件
export default eventBus;

1.3 Web API used

1.3.1 Element.clientHeight

The first Element.clientHeightis a read-only property with the following characteristics:

  • For those elements that do not define CSS or inline layout boxes, the API will return 0;
  • For the root element (html element) or the body element in quirks mode, the API will return the viewport height (without any scrollbars)
  • Otherwise, the API returns the height (in pixels) of the element's interior, inclusive content, paddingexcluding border, marginand horizontal scrollbar (if present).

In addition, the changed API will round the obtained value to an integer. If you need decimal results, you can use the element.getBoundingClientRect() method.

An example image is as follows:

clientHeight example image

For detailed documentation of this API, please refer to MDN - Element.clientHeight .

1.3.2 Element.getBoundingClientRect()

Element.getBoundingClientRect()The method returns an DOMRectobject that provides the size of the element and its position relative to the viewport.
This method has no parameters, and the return value is DOMRectan object. The properties of this object are as follows:

  • Width: is the width of the element itself
  • height: the height of the element itself
  • left(x): the distance from the starting position of the element to the left side of the window
  • right: the distance from the right side of the element to the left side of the window
  • bottom: the distance from the bottom of the element to the top of the window
  • top(y): the distance from the top of the element to the top of the window
  • x and y are equivalent to left and top

The schematic diagram is as follows:

getBoundingClientRect() sample image

For detailed documentation of this API, please refer to MDN - Element.getBoundingClientRect()

2. Basic introduction to image lazy loading instructions

2.1 The final realization effect

The final effect is as follows:
Picture lazy loading rendering

2.2 Registration and use of image lazy loading instructions

Since the image lazy loading instruction is frequently used in the personal blog system, I chose to register this instruction globally.

In addition, because I use the event bus method to communicate by myself, I also need to introduce the event bus configuration file—eventBus.js

So the code main.js入口文件for is as follows:

import Vue from "vue";
import App from "./App.vue";
import "./eventBus"; //引入事件总线
import vLazy from "./directives/lazy";
Vue.directive("lazy", vLazy); //全局注册指令
new Vue({
    
    
  render: (h) => h(App),
}).$mount("#app");

Example code using the v-lazy directive is as follows:

<template>
  <div class="container">
    <ul>
      <li v-for="img in imgs" :key="img.id">
        <img v-lazy="img.src" :alt="img.alt" :title="img.title" />
      </li>
    </ul>
  </div>
</template>
<script>
export default {
  data() {
    return {
      imgs: [
        {
          id: "",
          src: "",
          alt: "",
          title: "",
        },
      ],
    };
  },
  //下面的代码可以用组件混入来进行封装,从而优化代码结构
  methods: {
    //触发mainScroll事件
    handleMainScroll() {
      this.$bus.$emit("mainScroll", this.$refs.container);
    },
  },
  mounted() {
    //监听滚轮事件
    this.$refs.container.addEventListener("scroll", this.handleMainScroll);
  },
  beforeDestroy() {
    this.$bus.$emit("mainScroll");//参数传入undefined,表示dom元素已经不存在
    //取消监听滚轮事件
    this.$refs.container.removeEventListener("scroll", this.handleMainScroll);
  },

};
</script>

3. The principle of lazy loading of pictures

To achieve lazy loading of images, we must first consider the following four key issues:

  1. How to monitor the scrolling of the container's scroll bar?
  2. Which hook functions to use custom directives?
  3. How to judge whether the image img element is within the visible range of the user?
  4. How to handle loading of image img element?

3.1 How to monitor the scrolling of the scroll bar of the container?

For this problem, since my blog system uses the event bus method when dealing with the value transfer problem between other components, I also use this method for convenience. Of course, you can use other methods to solve this problem in actual scenarios. question.

lazy.jsSo we need to listen to the event bus eventBus in the v-lazy picture lazy loading instruction configuration file- file mainScroll事件, and for performance optimization, we need to perform the mainScroll event 事件防抖.

Among them, the event anti-shake tool function - debounce.jsthe code is as follows:

/**
 * @param {Function} fn 需要进行防抖操作的事件函数
 * @param {Number} duration 间隔时间
 * @returns {Function} 已进行防抖的函数
 */
export default function (fn, duration = 100) {
    
    
  let timer = null;
  return (...args) => {
    
    
    clearTimeout(timer);
    timer = setTimeout(() => {
    
    
      fn(...args);
    }, duration);
  };
}

Image lazy loading instruction configuration file - lazy.jsthe code of this part is as follows:

import eventBus from "@/eventBus"; //引入事件总线
import {
    
     debounce } from "@/utils"; //引入函数防抖工具函数

// 调用setImages函数,就可以处理那些符合条件的图片
function setImages() {
    
    }

//监听事件总线中的mainScroll事件,该事件触发时调用setImages函数来加载图片
eventBus.$on("mainScroll", debounce(setImages, 50));

3.2 Which hook functions are used for custom commands?

After scene analysis, I chose insertedthese unbindtwo hook functions to collect img information when the img element is just inserted into the parent node , and internally use an imgs array to store the collected information. When the instruction is unbound from the element , the imgs array clearing operation.

In addition, we also need to get the DOM node and src attribute value of the image img element

  • Since we bound the directive to the img' element, el参数its DOM node can be obtained through the custom directive hook function tree
  • Since we passed the src value to the command, we can bindings.value参数get its src attribute value by

So at this time, the image lazy loads the instruction configuration file— lazy.jsthe code of this part is as follows:

import eventBus from "@/eventBus"; //引入事件总线
import {
    
     debounce } from "@/utils"; //引入函数防抖工具函数

// 调用setImages函数,就可以处理那些符合条件的图片
function setImages() {
    
    }

//监听事件总线中的mainScroll事件,该事件触发时调用setImages函数来加载图片
eventBus.$on("mainScroll", debounce(setImages, 50));

//上面代码是3.1 如何监听容器的滚动条的滚动?
//下面代码是3.2 使用自定义指令哪些钩子函数?

let imgs = []; //存储收集到的的图片信息 当图片加载好后删除该图片信息

//调用setImage函数,就可以进行单张图片的加载
function setImage(img) {
    
    }

export default {
    
    
  inserted(el, bindings) {
    
    
    //刚插入父节点时 收集img节点信息
    const img = {
    
    
      dom: el, //img 元素DOM节点
      src: bindings.value, //img的src属性值
    };
    imgs.push(img); //先将图片信息存储到imgs数组
    setImage(img); // 立即判断该图片是否要加载
  },
  unbind(el) {
    
    
    //解绑时 删除 imgs 中的所有图片信息
    imgs = imgs.filter((img) => img.dom !== el);
  },
};

3.3 How to judge whether the image img element is within the visible range of the user?

For the above problem, we first split the problem:

  1. Get the user's visible range (viewport)

    • Since my blogging system only needs to consider the viewport height, I only use Element.clientHeightthis API. (If you need to consider the width, then use Element.clientWidth )
  2. Get the location information of the image img element

    • I use Element.getBoundingClientRect()this API.
  3. Determine whether the image img element is in the viewport

  • When img.getBoundingClientRect().top > 0, it means that the image is inside or below the viewport
    • When img.getBoundingClientRect().top <= document.documentElement.clientHeight, the img element is in the viewport
    • Otherwise, it is not in the viewport
  • When img.getBoundingClientRect().top < 0, it means that the image is inside or above the viewport
    • When -img.getBoundingClientRect().top <= img.getBoundingClientRect().height, the img element is within the viewport
    • Otherwise, it is not in the viewport

Image lazy loading instruction configuration file - lazy.jsthe code of this part is as follows:

import eventBus from "@/eventBus"; //引入事件总线
import {
    
     debounce } from "@/utils"; //引入函数防抖工具函数

let imgs = []; //存储收集到的的图片信息

// 调用setImages函数,就可以处理那些符合条件的图片
function setImages() {
    
    
  for (const img of imgs) {
    
    
    setImage(img); // 处理该图片
  }
}

//监听事件总线中的mainScroll事件,该事件触发时调用setImages函数来加载符合条件图片
eventBus.$on("mainScroll", debounce(setImages, 50));

//当图片加载好后删除该图片信息
export default {
    
    
  inserted(el, bindings) {
    
    
    //刚插入父节点时 收集img节点信息
    const img = {
    
    
      dom: el, //img 元素DOM节点
      src: bindings.value, //img的src属性值
    };
    imgs.push(img); //先将图片信息存储到imgs数组
    setImage(img); // 立即判断该图片是否要加载
  },
  unbind(el) {
    
    
    //解绑时 删除 imgs 中的所有图片信息
    imgs = imgs.filter((img) => img.dom !== el);
  },
};

//上面代码是3.1 如何监听容器的滚动条的滚动?+ 3.2 使用自定义指令哪些钩子函数?
//下面代码是3.3 如何判断图片 img 元素是否在用户的可见范围内?

//调用setImage函数,就可以进行单张图片的加载
function setImage(img) {
    
    
  const clientHeight = document.documentElement.clientHeight; //视口高度
  const rect = img.dom.getBoundingClientRect(); //图片的位置信息
  //取默认值150 是为了解决图片未加载成功时高度缺失的问题
  const height = rect.height || 150; //图片的高度

  // 判断该图片是否在视口范围内
  if (rect.top >= -height && rect.top <= clientHeight) {
    
    
    // 在视口范围内 进行相关处理操作
  } else {
    
    
    // 不在视口范围内 不进行操作
  }
}

3.4 How to handle the loading of image img element?

From the effect diagram, we can see that all img elements are a default GIF image at the beginning — defaultGif, when the img element enters the viewport range, start loading the image, and then replace it after loading.

Here I also perform an optimization operation, which is to create a new one first Image 对象实例, instead of the img element to load the image, because it will be triggered after the image is loaded, so onload事件we only need to onload事件rewrite it, and perform the src attribute replacement operation of the img element inside, so that the solution is solved Fixed the situation where the image is blank during loading.

So the image lazy loading instruction configuration file - lazy.jsthe complete code is as follows:

import eventBus from "@/eventBus"; //引入事件总线
import {
    
     debounce } from "@/utils"; //引入函数防抖工具函数
import defaultGif from "@/assets/default.gif"; //在assets静态文件夹下放入默认图

let imgs = []; //存储收集到的且未加载的图片信息

//调用setImage函数,就可以进行单张图片的加载
function setImage(img) {
    
    
  img.dom.src = defaultGif; // 先暂时使用默认图片
  const clientHeight = document.documentElement.clientHeight; //视口高度
  const rect = img.dom.getBoundingClientRect(); //图片的位置信息
  //取默认值150 是为了解决图片未加载成功时 高度缺失的问题
  const height = rect.height || 150; //图片的高度
  // 判断该图片是否在视口范围内
  if (-rect.top <= height && rect.top <= clientHeight) {
    
    
    // 在视口范围内 进行相关处理操作
    const tempImg = new Image(); //新建Image对象实例
    //改写onload事件
    tempImg.onload = function () {
    
    
      // 当图片加载完成之后
      img.dom.src = img.src; //替换img元素的src属性
    };
    tempImg.src = img.src;
    imgs = imgs.filter((i) => i !== img); //将已加载好的图片进行删除
  }
}

// 调用setImages函数,就可以处理那些符合条件的图片
function setImages() {
    
    
  for (const img of imgs) {
    
    
    setImage(img); // 处理该图片
  }
}

//监听事件总线中的mainScroll事件,该事件触发时调用setImages函数来加载符合条件图片
eventBus.$on("mainScroll", debounce(setImages, 50));

//当图片加载好后删除该图片信息
export default {
    
    
  inserted(el, bindings) {
    
    
    //刚插入父节点时 收集img节点信息
    const img = {
    
    
      dom: el, //img 元素DOM节点
      src: bindings.value, //img的src属性值
    };
    imgs.push(img); //先将图片信息存储到imgs数组
    setImage(img); // 立即判断该图片是否要加载
  },
  unbind(el) {
    
    
    //解绑时 清空 imgs
    imgs = imgs.filter((img) => img.dom !== el);
  },
};

Since friends in the comment area shared it Intersection Observer API, I found out that this method is indeed simpler and has better performance.

So I wrote a detailed explanation of the Intersection Observer API . Friends who like it can go and have a look after reading this article.

I also used Intersection Observer APIthe method to realize the effect of lazy loading of pictures, you can go and have a look. Lazy loading instructions for custom images in Vue2 2.0

epilogue

This is the best answer in the knowledge I know so far, and of course there may be some misunderstandings.

So if you have doubts about this article, you can leave a message in the comment area, and everyone is welcome to point out the wrong views in the article.

Code words are not easy, friends who find it helpful will like it, and follow it.

Guess you like

Origin blog.csdn.net/forward_xx/article/details/126976237