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

Vue生命周期钩子 性能优化的最佳实践分享

2021-04-234.5k 阅读

Vue 生命周期钩子概述

Vue 实例从创建到销毁的过程,就是生命周期。在这个过程中,Vue 提供了一系列的生命周期钩子函数,让开发者可以在特定阶段执行自定义逻辑。这些钩子函数在 Vue 应用的性能优化方面起着至关重要的作用。

Vue 实例的生命周期钩子主要包括:

  1. beforeCreate:在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用。此时,实例的 data 和 methods 等属性还未初始化,通常用于一些初始化操作,但由于无法访问实例的属性,实际应用场景较少。
new Vue({
  beforeCreate() {
    console.log('beforeCreate 钩子被调用');
  }
})
  1. created:实例已经创建完成之后被调用。在这一步,实例已完成数据观测 (data observer)、属性和方法的运算,watch/event 事件回调。此时可以访问 data 和 methods,但 DOM 还未创建,无法操作 DOM。常用于异步数据请求,在页面渲染前获取数据。
new Vue({
  data() {
    return {
      message: ''
    }
  },
  created() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 模拟异步数据请求
      setTimeout(() => {
        this.message = '从服务器获取的数据';
      }, 1000);
    }
  }
})
  1. beforeMount:在挂载开始之前被调用:相关的 render 函数首次被调用。此时虚拟 DOM 已创建,但真实 DOM 还未插入文档,无法直接操作真实 DOM。
new Vue({
  beforeMount() {
    console.log('beforeMount 钩子被调用');
  }
})
  1. mounted:实例被挂载后调用,这时 el 被新创建的 vm.$el 替换,并挂载到实例上去了。此时可以操作真实 DOM,进行一些需要 DOM 元素的初始化操作,如初始化第三方插件等。
<template>
  <div id="app">
    <h1 ref="title">{{ message }}</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    }
  },
  mounted() {
    console.log(this.$refs.title.textContent);
    // 可以对标题进行进一步操作,比如修改样式
    this.$refs.title.style.color = 'red';
  }
}
</script>
  1. beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在这个钩子中进一步观察数据变化,避免不必要的更新。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '初始消息'
    }
  },
  methods: {
    updateMessage() {
      this.message = '更新后的消息';
    }
  },
  beforeUpdate() {
    console.log('数据即将更新,当前 message:', this.message);
  }
}
</script>
  1. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。注意在这个钩子函数中,如果对数据进行修改,可能会导致无限循环更新。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">更新消息</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '初始消息'
    }
  },
  methods: {
    updateMessage() {
      this.message = '更新后的消息';
    }
  },
  updated() {
    console.log('数据已更新,当前 message:', this.message);
  }
}
</script>
  1. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用,可以进行一些清理工作,如清除定时器、解绑事件等。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="destroyInstance">销毁实例</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '实例存在'
    }
  },
  methods: {
    destroyInstance() {
      this.$destroy();
    }
  },
  beforeDestroy() {
    console.log('实例即将销毁');
  }
}
</script>
  1. destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="destroyInstance">销毁实例</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: '实例存在'
    }
  },
  methods: {
    destroyInstance() {
      this.$destroy();
    }
  },
  destroyed() {
    console.log('实例已销毁');
  }
}
</script>

利用生命周期钩子进行性能优化

  1. 减少不必要的渲染beforeUpdate 钩子中,可以通过对比更新前后的数据,判断是否真的需要进行 DOM 更新。如果数据没有实质性变化,可以阻止更新,从而减少不必要的渲染开销。
<template>
  <div>
    <p>{{ user.name }}</p>
    <button @click="updateUser">更新用户</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      user: {
        name: 'John',
        age: 30
      },
      prevUser: null
    }
  },
  methods: {
    updateUser() {
      // 模拟数据更新
      this.user.age++;
    }
  },
  beforeUpdate() {
    if (this.prevUser && JSON.stringify(this.prevUser) === JSON.stringify(this.user)) {
      // 数据无实质变化,阻止更新
      return false;
    }
    this.prevUser = {...this.user };
  }
}
</script>
  1. 优化数据请求created 钩子中进行异步数据请求时,可以合理控制请求时机和频率。例如,如果组件可能在短时间内多次创建和销毁,可以考虑缓存数据,避免重复请求。
<template>
  <div>
    <ul>
      <li v - for="item in items" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

<script>
const cache = {};
export default {
  data() {
    return {
      items: []
    }
  },
  created() {
    if (cache['items']) {
      this.items = cache['items'];
    } else {
      this.fetchData();
    }
  },
  methods: {
    fetchData() {
      // 模拟异步数据请求
      setTimeout(() => {
        const newItems = [
          { id: 1, name: 'Item 1' },
          { id: 2, name: 'Item 2' }
        ];
        this.items = newItems;
        cache['items'] = newItems;
      }, 1000);
    }
  }
}
</script>
  1. 组件销毁时的清理工作beforeDestroy 钩子中进行清理工作,如清除定时器、解绑事件等,能够避免内存泄漏,提高应用性能。
<template>
  <div>
    <p>{{ counter }}</p>
    <button @click="startCounting">开始计数</button>
    <button @click="stopComponent">停止组件</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      counter: 0,
      timer: null
    }
  },
  methods: {
    startCounting() {
      this.timer = setInterval(() => {
        this.counter++;
      }, 1000);
    },
    stopComponent() {
      this.$destroy();
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearInterval(this.timer);
    }
  }
}
</script>
  1. 延迟挂载操作 对于一些复杂的组件或包含大量 DOM 操作的组件,可以在 beforeMount 钩子中进行延迟挂载。例如,通过 requestAnimationFrame 延迟挂载,使页面渲染更加流畅。
<template>
  <div id="complex - component">
    <!-- 复杂的 DOM 结构 -->
    <h1>复杂组件</h1>
    <p>这是一个包含很多内容的组件</p>
  </div>
</template>

<script>
export default {
  beforeMount() {
    requestAnimationFrame(() => {
      // 这里执行挂载相关操作,如插入 DOM 等
      console.log('延迟挂载操作');
    });
  }
}
</script>
  1. 结合 keep - alive 优化 keep - alive 是 Vue 提供的一个抽象组件,它可以缓存不活动的组件实例,而不是销毁它们。在使用 keep - alive 时,组件会新增 activateddeactivated 两个生命周期钩子。
  • activated:在 keep - alive 组件激活时调用,可以在此处进行一些需要重新执行的操作,如重新获取数据等。
  • deactivated:在 keep - alive 组件停用时调用,可以在此处进行一些清理操作。
<template>
  <div>
    <keep - alive>
      <component :is="currentComponent"></component>
    </keep - alive>
    <button @click="switchComponent">切换组件</button>
  </div>
</template>

<script>
import ComponentA from './ComponentA.vue';
import ComponentB from './ComponentB.vue';

export default {
  data() {
    return {
      currentComponent: 'ComponentA'
    }
  },
  components: {
    ComponentA,
    ComponentB
  },
  methods: {
    switchComponent() {
      this.currentComponent = this.currentComponent === 'ComponentA'? 'ComponentB' : 'ComponentA';
    }
  }
}
</script>

ComponentA.vue 中:

<template>
  <div>
    <h1>组件 A</h1>
    <p>这是组件 A 的内容</p>
  </div>
</template>

<script>
export default {
  activated() {
    console.log('组件 A 被激活');
    // 可以在此处重新获取数据等操作
  },
  deactivated() {
    console.log('组件 A 停用');
    // 可以在此处进行清理操作
  }
}
</script>

同理,在 ComponentB.vue 中也可以定义 activateddeactivated 钩子进行相应的处理。通过这种方式,可以避免频繁创建和销毁组件带来的性能开销。

  1. 在 mounted 中合理操作 DOMmounted 钩子中操作 DOM 时,要注意操作的频率和复杂度。如果需要对大量 DOM 元素进行操作,可以考虑使用文档片段 (DocumentFragment) 来减少对真实 DOM 的直接操作次数。
<template>
  <div id="app">
    <ul ref="list"></ul>
  </div>
</template>

<script>
export default {
  mounted() {
    const fragment = document.createDocumentFragment();
    const data = ['Item 1', 'Item 2', 'Item 3'];
    data.forEach(item => {
      const li = document.createElement('li');
      li.textContent = item;
      fragment.appendChild(li);
    });
    this.$refs.list.appendChild(fragment);
  }
}
</script>

通过先将 DOM 操作在文档片段中完成,最后一次性添加到真实 DOM 中,能够减少页面重排和重绘的次数,提高性能。

  1. 在 updated 中避免无限循环updated 钩子中,如果不小心再次修改数据,可能会导致无限循环更新。例如:
<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  methods: {
    increment() {
      this.count++;
    }
  },
  updated() {
    // 错误示范,会导致无限循环
    this.count++;
  }
}
</script>

为了避免这种情况,需要在 updated 钩子中谨慎操作数据,确保数据修改是有条件的,并且不会引发新一轮的更新。比如:

<template>
  <div>
    <p>{{ count }}</p>
    <button @click="increment">增加计数</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
      shouldUpdate: false
    }
  },
  methods: {
    increment() {
      this.count++;
      this.shouldUpdate = true;
    }
  },
  updated() {
    if (this.shouldUpdate) {
      // 进行一些额外的操作,但不会再次触发更新
      console.log('更新后执行额外操作');
      this.shouldUpdate = false;
    }
  }
}
</script>

不同场景下的性能优化实践

  1. 单页面应用 (SPA) 的性能优化 在 SPA 中,页面切换频繁,合理利用生命周期钩子可以显著提升性能。例如,在页面切换时,可以在 beforeDestroy 钩子中取消未完成的异步请求,避免资源浪费。
<template>
  <div>
    <button @click="fetchData">获取数据</button>
    <p v - if="data">{{ data }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null,
      timer: null
    }
  },
  methods: {
    fetchData() {
      // 模拟异步请求
      this.timer = setTimeout(() => {
        this.data = '从服务器获取的数据';
      }, 5000);
    }
  },
  beforeDestroy() {
    if (this.timer) {
      clearTimeout(this.timer);
    }
  }
}
</script>

同时,在 activated 钩子中重新获取数据,确保页面激活时数据是最新的。

<template>
  <div>
    <p v - if="data">{{ data }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null
    }
  },
  activated() {
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 模拟异步请求
      setTimeout(() => {
        this.data = '从服务器获取的数据';
      }, 1000);
    }
  }
}
</script>
  1. 大型表单组件的性能优化 对于大型表单组件,在 beforeUpdate 钩子中可以通过防抖 (Debounce) 或节流 (Throttle) 技术来控制表单数据变化时的更新频率。
<template>
  <div>
    <form>
      <input v - model="formData.name" type="text" placeholder="姓名">
      <input v - model="formData.age" type="number" placeholder="年龄">
    </form>
  </div>
</template>

<script>
export default {
  data() {
    return {
      formData: {
        name: '',
        age: 0
      },
      debounceTimer: null
    }
  },
  beforeUpdate() {
    if (this.debounceTimer) {
      clearTimeout(this.debounceTimer);
    }
    this.debounceTimer = setTimeout(() => {
      // 进行表单数据更新操作
      console.log('表单数据更新:', this.formData);
    }, 300);
  }
}
</script>

通过防抖处理,只有在用户停止输入 300 毫秒后才会执行表单数据更新操作,减少了不必要的更新,提高了性能。 3. 列表组件的性能优化 在列表组件中,当数据量较大时,利用 updated 钩子结合虚拟列表技术可以优化性能。虚拟列表只渲染可见区域的列表项,减少 DOM 渲染数量。

<template>
  <div>
    <div ref="listContainer" class="list - container">
      <div v - for="(item, index) in visibleItems" :key="index" class="list - item">{{ item }}</div>
    </div>
  </div>
</template>

<script>
export default {
  data() {
    return {
      allItems: Array.from({ length: 1000 }, (_, i) => `Item ${i + 1}`),
      visibleItems: [],
      startIndex: 0,
      endIndex: 10
    }
  },
  updated() {
    this.updateVisibleItems();
  },
  methods: {
    updateVisibleItems() {
      this.visibleItems = this.allItems.slice(this.startIndex, this.endIndex);
    },
    handleScroll() {
      const scrollTop = this.$refs.listContainer.scrollTop;
      const itemHeight = 40;
      this.startIndex = Math.floor(scrollTop / itemHeight);
      this.endIndex = this.startIndex + 10;
    }
  },
  mounted() {
    this.$refs.listContainer.addEventListener('scroll', this.handleScroll);
  },
  beforeDestroy() {
    this.$refs.listContainer.removeEventListener('scroll', this.handleScroll);
  }
}
</script>

mounted 钩子中监听列表容器的滚动事件,在 updated 钩子中更新可见列表项,通过这种方式,无论列表数据量多大,始终只渲染少量可见的列表项,提高了列表组件的性能。

  1. 动画组件的性能优化 对于包含动画的组件,在 mounted 钩子中初始化动画,在 beforeDestroy 钩子中停止动画,可以避免动画相关的内存泄漏和性能问题。
<template>
  <div>
    <div ref="animationBox" class="animation - box" :style="{ transform: `translateX(${animationValue}px)` }"></div>
    <button @click="startAnimation">开始动画</button>
    <button @click="stopComponent">停止组件</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      animationValue: 0,
      animationInterval: null
    }
  },
  methods: {
    startAnimation() {
      this.animationInterval = setInterval(() => {
        this.animationValue += 5;
        if (this.animationValue >= 300) {
          clearInterval(this.animationInterval);
        }
      }, 50);
    },
    stopComponent() {
      this.$destroy();
    }
  },
  mounted() {
    // 可以在此处进行动画相关的初始化设置,如设置初始样式等
  },
  beforeDestroy() {
    if (this.animationInterval) {
      clearInterval(this.animationInterval);
    }
  }
}
</script>

通过在 mountedbeforeDestroy 钩子中分别进行动画的初始化和清理,确保动画组件在使用过程中的性能和资源管理。

  1. 嵌套组件的性能优化 在嵌套组件中,子组件的生命周期钩子也会对整体性能产生影响。例如,在父组件的 beforeUpdate 钩子中,可以通过 $emit 事件通知子组件进行相应的优化操作。
<!-- 父组件 -->
<template>
  <div>
    <child - component :data="parentData" @parentUpdate="handleParentUpdate"></child - component>
    <button @click="updateParentData">更新父组件数据</button>
  </div>
</template>

<script>
import ChildComponent from './ChildComponent.vue';

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentData: {
        value: '初始值'
      }
    }
  },
  methods: {
    updateParentData() {
      this.parentData.value = '更新后的值';
    },
    handleParentUpdate() {
      // 父组件可以在此处进行一些子组件更新前的优化操作
      console.log('父组件通知子组件更新,进行优化操作');
    }
  },
  beforeUpdate() {
    this.$emit('parentUpdate');
  }
}
</script>
<!-- 子组件 -->
<template>
  <div>
    <p>{{ data.value }}</p>
  </div>
</template>

<script>
export default {
  props: ['data'],
  beforeUpdate() {
    console.log('子组件即将更新');
    // 子组件可以在此处进行一些更新前的优化操作
  }
}
</script>

通过父子组件之间的事件通信和生命周期钩子的配合,能够在嵌套组件场景下进行有效的性能优化。

  1. 服务端渲染 (SSR) 中的性能优化 在服务端渲染中,Vue 的生命周期钩子也有特殊的应用场景。例如,在 created 钩子中进行数据预取时,要考虑服务器的性能和资源消耗。可以通过缓存机制来减少重复的数据请求。
// server - side code
import Vue from 'vue';
import App from './App.vue';

const server = new Vue({
  created() {
    const cache = {};
    if (!cache['data']) {
      // 模拟异步数据请求
      setTimeout(() => {
        cache['data'] = '从服务器获取的数据';
        this.$data.serverData = cache['data'];
      }, 1000);
    } else {
      this.$data.serverData = cache['data'];
    }
  },
  render: h => h(App)
});

export default server;

同时,在客户端激活 (hydration) 阶段,要确保客户端和服务器端的数据一致性,避免重复渲染。通过合理利用 activated 钩子等,可以在 SSR 场景下实现良好的性能优化。

性能监控与优化效果评估

  1. 使用浏览器开发者工具 浏览器的开发者工具提供了强大的性能分析功能。例如,在 Chrome 浏览器中,可以使用 Performance 面板来记录和分析 Vue 应用的性能。
  • 录制性能数据:打开 Chrome 开发者工具,切换到 Performance 面板,点击录制按钮,然后在应用中执行相关操作,如页面加载、组件切换、数据更新等。操作完成后,停止录制,即可得到详细的性能报告。
  • 分析性能瓶颈:在性能报告中,可以查看各个生命周期钩子函数的执行时间,以及渲染、脚本执行等阶段的耗时情况。例如,如果发现 mounted 钩子执行时间过长,可能是在该钩子中进行了过于复杂的 DOM 操作,需要进一步优化。
  1. 自定义性能指标 除了使用浏览器自带的性能工具,还可以自定义性能指标来评估优化效果。例如,在 created 钩子中记录开始时间,在 mounted 钩子中记录结束时间,计算组件从创建到挂载完成的时间。
<template>
  <div>
    <h1>自定义性能指标示例</h1>
  </div>
</template>

<script>
export default {
  data() {
    return {
      creationStartTime: null,
      mountEndTime: null
    }
  },
  created() {
    this.creationStartTime = new Date().getTime();
  },
  mounted() {
    this.mountEndTime = new Date().getTime();
    const totalTime = this.mountEndTime - this.creationStartTime;
    console.log(`组件从创建到挂载完成耗时: ${totalTime} 毫秒`);
  }
}
</script>

通过这种方式,可以针对具体组件或功能模块,量化性能优化的效果,便于对比不同优化方案的优劣。 3. 性能测试框架 使用性能测试框架,如 Lighthouse,可以对整个 Vue 应用进行全面的性能测试。Lighthouse 不仅可以评估性能指标,还能提供详细的优化建议。

  • 安装与运行:可以通过 Chrome 浏览器扩展程序或命令行工具安装 Lighthouse。在命令行中运行 lighthouse <url>,其中 <url> 是 Vue 应用的 URL,即可生成性能报告。
  • 优化建议分析:Lighthouse 的报告中会指出应用在性能、可访问性、最佳实践等方面的得分和问题。对于性能相关的问题,如首次内容绘制时间过长、最大内容绘制时间不理想等,可以结合 Vue 生命周期钩子的优化方法,有针对性地进行改进。

常见性能优化误区与解决方法

  1. 过度优化
  • 误区:有些开发者为了追求极致性能,在不必要的地方进行复杂的优化,导致代码复杂度增加,维护成本上升,反而影响开发效率和整体性能。例如,在数据量很小的列表组件中,过度使用虚拟列表技术,增加了代码的复杂性,却没有带来明显的性能提升。
  • 解决方法:在进行性能优化之前,先进行性能分析,确定性能瓶颈所在。只对真正影响性能的部分进行优化,避免过度优化。同时,要平衡优化带来的收益和成本,确保优化是有价值的。
  1. 忽略组件卸载时的清理
  • 误区:在组件销毁时,没有正确清理定时器、事件监听器等资源,导致内存泄漏。例如,在 mounted 钩子中添加了事件监听器,但在 beforeDestroy 钩子中没有移除,随着组件的频繁创建和销毁,内存占用会不断增加。
  • 解决方法:养成在 beforeDestroy 钩子中进行清理工作的习惯。对于定时器,使用 clearIntervalclearTimeout 清除;对于事件监听器,使用 removeEventListener 移除。同时,可以使用工具如 Chrome 开发者工具的 Memory 面板来检测内存泄漏问题。
  1. 不合理的数据更新触发
  • 误区:在 updated 钩子中不小心再次修改数据,导致无限循环更新。或者在不必要的数据变化时触发更新,例如在计算属性依赖的某个值发生微小变化,但对实际渲染没有影响时,也触发了更新。
  • 解决方法:在 updated 钩子中谨慎操作数据,添加必要的条件判断,避免无限循环。对于数据更新的触发,要仔细分析数据之间的依赖关系,合理使用计算属性和 watcher,确保只有在数据发生实质性变化且对渲染有影响时才触发更新。
  1. 未充分利用缓存
  • 误区:在多次进行相同的数据请求或计算时,没有利用缓存,导致重复的开销。例如,在 created 钩子中多次发起相同的异步数据请求,而没有检查是否已经有缓存数据。
  • 解决方法:在合适的生命周期钩子中实现缓存机制。可以使用简单的对象缓存,也可以结合浏览器的本地存储或服务端的缓存策略。在进行数据请求或计算之前,先检查缓存中是否已经存在所需数据,若存在则直接使用,避免重复操作。

通过避免这些常见的性能优化误区,并结合合理的性能优化方法,能够有效地提升 Vue 应用的性能,为用户提供更加流畅的体验。在实际开发中,要不断积累经验,根据具体的应用场景和需求,灵活运用 Vue 生命周期钩子进行性能优化。