MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Vue生命周期钩子 如何利用钩子实现懒加载功能

2023-03-163.9k 阅读

Vue生命周期钩子概述

在深入探讨如何利用Vue生命周期钩子实现懒加载功能之前,我们先来全面了解一下Vue的生命周期钩子。Vue实例从创建到销毁的过程,就是它的生命周期。在这个过程中,Vue提供了一系列的生命周期钩子函数,让开发者能够在特定的阶段执行自定义的逻辑。

常用的生命周期钩子

  1. beforeCreate:在实例初始化之后,数据观测(data observer)和event/watcher事件配置之前被调用。此时,实例的data和methods等属性都还未初始化,无法访问到这些数据。例如:
new Vue({
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeCreate() {
    console.log(this.message); // 这里会输出undefined,因为data还未初始化
  }
})
  1. created:实例已经创建完成,数据观测、属性和方法的运算、watch/event事件回调都已配置好。此时可以访问到data和methods等数据。例如:
new Vue({
  data() {
    return {
      message: 'Hello'
    }
  },
  created() {
    console.log(this.message); // 这里会输出'Hello'
  }
})
  1. beforeMount:在挂载开始之前被调用:相关的render函数首次被调用。此时,虚拟DOM已经创建完成,但是还没有挂载到真实DOM上。
new Vue({
  template: '<div>{{ message }}</div>',
  data() {
    return {
      message: 'Hello'
    }
  },
  beforeMount() {
    console.log(this.$el); // 这里会输出undefined,因为还未挂载到真实DOM
  }
})
  1. mounted:实例被挂载后调用,这时el被新创建的vm.$el替换,并挂载到实例上去了。可以在这里操作真实DOM,发起异步请求等。例如:
new Vue({
  template: '<div>{{ message }}</div>',
  data() {
    return {
      message: 'Hello'
    }
  },
  mounted() {
    console.log(this.$el.textContent); // 这里会输出'Hello',可以操作真实DOM
  }
})
  1. beforeUpdate:数据更新时调用,发生在虚拟DOM重新渲染和打补丁之前。可以在这个钩子中获取更新前的状态。
new Vue({
  template: '<div>{{ message }}</div>',
  data() {
    return {
      message: 'Hello'
    }
  },
  methods: {
    updateMessage() {
      this.message = 'World';
    }
  },
  beforeUpdate() {
    console.log('更新前的message:', this.message); // 这里输出更新前的message值
  }
})
  1. updated:数据更新后调用,发生在虚拟DOM重新渲染和打补丁之后。注意在这个钩子函数中,由于数据已经更新,如果再次修改数据可能会导致无限循环。
new Vue({
  template: '<div>{{ message }}</div>',
  data() {
    return {
      message: 'Hello'
    }
  },
  methods: {
    updateMessage() {
      this.message = 'World';
    }
  },
  updated() {
    console.log('更新后的message:', this.message); // 这里输出更新后的message值
  }
})
  1. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用,可以在这里清理定时器、解绑事件等。
new Vue({
  data() {
    return {
      timer: null
    }
  },
  created() {
    this.timer = setInterval(() => {
      console.log('定时器在运行');
    }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.timer);
    console.log('定时器已清理');
  }
})
  1. destroyed:实例销毁后调用。调用后,所有的事件监听器被移除,所有的子实例也都被销毁。此时实例已不可用。

懒加载的概念与应用场景

懒加载,也称为延迟加载,是一种在需要的时候才加载资源的技术。在前端开发中,它主要用于图片、组件等资源的加载。当页面中有大量图片或者一些不急需展示的组件时,如果一开始就全部加载,会导致页面加载时间过长,影响用户体验。通过懒加载,只有当这些资源进入浏览器的可视区域时,才会触发加载操作。

图片懒加载场景

比如一个电商网站的商品列表页,有大量商品图片。如果用户打开页面时所有图片都加载,不仅会消耗大量带宽,还会使页面加载缓慢。而采用懒加载,当用户滚动页面,图片即将进入可视区域时才加载,这样可以大大提高页面的初始加载速度,节省用户流量。

组件懒加载场景

在单页应用(SPA)中,可能有一些组件只有在特定情况下才会用到,比如用户点击某个按钮后才展示的弹窗组件。如果一开始就将所有组件都加载进来,会增加应用的初始加载体积。使用懒加载,只有在需要展示该组件时才加载,能有效减少初始加载时间,提升用户体验。

利用Vue生命周期钩子实现图片懒加载

使用mounted钩子初始化懒加载

在Vue组件中,我们可以利用mounted钩子来初始化图片的懒加载逻辑。首先,我们需要给图片设置一个自定义属性(比如data - src)来存储真实的图片地址,而src属性则可以先设置为一个占位符或者空字符串。

<template>
  <div>
    <img v - for="(image, index) in images" :key="index" :data - src="image.src" :src="image.placeholder" @load="handleImageLoad(index)" @error="handleImageError(index)" class="lazy - load - img">
  </div>
</template>

<script>
export default {
  data() {
    return {
      images: [
        { src: 'https://example.com/image1.jpg', placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' },
        { src: 'https://example.com/image2.jpg', placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' }
      ]
    };
  },
  mounted() {
    this.initLazyLoad();
  },
  methods: {
    initLazyLoad() {
      const lazyImages = document.querySelectorAll('.lazy - load - img');
      const observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
          }
        });
      });
      lazyImages.forEach(image => {
        observer.observe(image);
      });
    },
    handleImageLoad(index) {
      console.log(`图片 ${index} 加载成功`);
    },
    handleImageError(index) {
      console.log(`图片 ${index} 加载失败`);
    }
  }
};
</script>

<style>
.lazy - load - img {
  width: 200px;
  height: 200px;
  object - fit: cover;
}
</style>

在上述代码中,mounted钩子中调用了initLazyLoad方法。该方法使用了IntersectionObserver API,它可以异步观察目标元素与其祖先元素或视口(viewport)交叉变化情况。当图片进入视口时(即isIntersecting为true),将data - src的值赋给src,从而触发图片加载,并停止对该图片的观察。同时,通过@load和@error事件监听图片的加载成功和失败情况。

结合beforeDestroy钩子清理资源

当组件被销毁时,我们需要清理IntersectionObserver相关的资源,以避免内存泄漏。这时候可以使用beforeDestroy钩子。

<template>
  <div>
    <img v - for="(image, index) in images" :key="index" :data - src="image.src" :src="image.placeholder" @load="handleImageLoad(index)" @error="handleImageError(index)" class="lazy - load - img">
  </div>
</template>

<script>
export default {
  data() {
    return {
      images: [
        { src: 'https://example.com/image1.jpg', placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' },
        { src: 'https://example.com/image2.jpg', placeholder: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' }
      ],
      observer: null
    };
  },
  mounted() {
    this.initLazyLoad();
  },
  methods: {
    initLazyLoad() {
      const lazyImages = document.querySelectorAll('.lazy - load - img');
      this.observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
          if (entry.isIntersecting) {
            const img = entry.target;
            img.src = img.dataset.src;
            observer.unobserve(img);
          }
        });
      });
      lazyImages.forEach(image => {
        this.observer.observe(image);
      });
    },
    handleImageLoad(index) {
      console.log(`图片 ${index} 加载成功`);
    },
    handleImageError(index) {
      console.log(`图片 ${index} 加载失败`);
    }
  },
  beforeDestroy() {
    if (this.observer) {
      const lazyImages = document.querySelectorAll('.lazy - load - img');
      lazyImages.forEach(image => {
        this.observer.unobserve(image);
      });
      this.observer.disconnect();
    }
  }
};
</script>

<style>
.lazy - load - img {
  width: 200px;
  height: 200px;
  object - fit: cover;
}
</style>

在beforeDestroy钩子中,我们首先判断observer是否存在,如果存在,则对所有懒加载图片停止观察,并断开IntersectionObserver的连接,释放资源。

利用Vue生命周期钩子实现组件懒加载

使用异步组件和beforeRouteEnter钩子

在Vue Router中,我们可以利用异步组件和beforeRouteEnter钩子来实现组件的懒加载。异步组件是一种在需要的时候才加载的组件,而beforeRouteEnter钩子在进入路由前被调用。

// router.js
import Vue from 'vue';
import Router from 'vue - router';

Vue.use(Router);

const Home = () => import('./views/Home.vue');
const LazyLoadedComponent = () => import('./views/LazyLoadedComponent.vue');

const router = new Router({
  routes: [
    {
      path: '/',
      name: 'Home',
      component: Home
    },
    {
      path: '/lazy',
      name: 'LazyLoadedComponent',
      component: LazyLoadedComponent
    }
  ]
});

export default router;
<!-- Home.vue -->
<template>
  <div>
    <h1>首页</h1>
    <router - link to="/lazy">跳转到懒加载组件</router - link>
  </div>
</template>

<script>
export default {
  name: 'Home'
};
</script>

<style scoped>
</style>
<!-- LazyLoadedComponent.vue -->
<template>
  <div>
    <h1>懒加载组件</h1>
  </div>
</template>

<script>
export default {
  name: 'LazyLoadedComponent'
};
</script>

<style scoped>
</style>

在上述代码中,我们通过() => import('./views/LazyLoadedComponent.vue')这种方式定义了一个异步组件。当路由匹配到/lazy时,才会加载LazyLoadedComponent.vue组件。如果我们想在组件加载前做一些逻辑处理,可以使用beforeRouteEnter钩子。

<!-- LazyLoadedComponent.vue -->
<template>
  <div>
    <h1>懒加载组件</h1>
  </div>
</template>

<script>
export default {
  name: 'LazyLoadedComponent',
  beforeRouteEnter(to, from, next) {
    // 这里可以做一些加载前的逻辑,比如检查权限
    console.log('即将进入懒加载组件,做一些预处理');
    next();
  }
};
</script>

<style scoped>
</style>

在beforeRouteEnter钩子中,我们可以进行一些权限检查、数据预加载等操作,然后通过next()继续进入组件。

使用keep - alive和activated钩子优化懒加载组件

如果懒加载组件需要被缓存,以避免重复加载和初始化,可以结合keep - alive组件和activated钩子。

<template>
  <div>
    <keep - alive>
      <router - view></router - view>
    </keep - alive>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style>
</style>
<!-- LazyLoadedComponent.vue -->
<template>
  <div>
    <h1>懒加载组件</h1>
  </div>
</template>

<script>
export default {
  name: 'LazyLoadedComponent',
  data() {
    return {
      count: 0
    };
  },
  activated() {
    console.log('组件被激活,可在此处更新数据');
    // 可以在这里重新请求数据或者更新组件状态
  }
};
</script>

<style scoped>
</style>

当组件被keep - alive缓存后,再次进入组件时不会重新创建,而是触发activated钩子。我们可以在activated钩子中进行数据更新等操作,以保证组件展示的数据是最新的。

实现懒加载时的性能优化与注意事项

性能优化

  1. 合理设置占位符:在图片懒加载中,占位符的设置很重要。使用合适的占位符(如1px透明图片或者简单的loading动画)可以让用户在图片加载前有一个预期,同时避免页面布局的跳动。
  2. 批量加载:如果有多个资源需要懒加载,可以考虑批量加载。比如,当一个图片即将进入视口时,同时检查附近的其他图片是否也即将进入视口,一次性加载多个图片,减少请求次数。
  3. 优化IntersectionObserver配置:IntersectionObserver的阈值(threshold)可以根据实际需求进行调整。默认值为0,表示元素的任何部分可见时就触发回调。如果设置为0.5,则元素至少有50%可见时才触发回调。合理设置阈值可以避免不必要的加载。

注意事项

  1. 兼容性问题:IntersectionObserver API在一些旧浏览器中不支持,需要使用polyfill来进行兼容处理。可以通过引入intersection - observer库来解决兼容性问题。
  2. SEO问题:对于图片懒加载,搜索引擎爬虫可能无法正确获取图片内容,影响SEO。可以通过设置aria - hidden="true"并添加alt属性来提供图片的替代文本,同时可以考虑在服务器端渲染(SSR)中对图片进行预处理,确保搜索引擎能够正确索引图片。
  3. 内存泄漏:在使用IntersectionObserver时,如果不及时清理观察,可能会导致内存泄漏。如前文所述,要在组件销毁时(beforeDestroy钩子)停止观察并断开连接。
  4. 懒加载与动画冲突:如果图片或组件有进入动画,可能会与懒加载逻辑冲突。比如,动画可能会导致元素提前进入视口,触发懒加载。可以通过设置动画延迟或者调整懒加载的触发条件来解决这个问题。

通过合理利用Vue的生命周期钩子,我们能够高效地实现图片和组件的懒加载功能,提升应用的性能和用户体验。同时,在实现过程中要注意性能优化和各种潜在问题,确保应用的稳定性和兼容性。