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

Vue中组件的生命周期优化策略

2022-09-043.8k 阅读

组件生命周期基础回顾

在深入探讨 Vue 组件生命周期优化策略之前,我们先来回顾一下 Vue 组件生命周期的基础知识。Vue 组件的生命周期可以分为创建、挂载、更新和销毁四个阶段,每个阶段都有对应的生命周期钩子函数。

  • 创建阶段
    • beforeCreate:在实例初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用。此时,组件实例的 data 和 methods 等属性还未初始化,无法访问。
    • created:在实例创建完成后被立即调用。此时,数据观测、属性和方法的运算都已完成,但是还未挂载到 DOM 上,$el 属性还不存在。
<template>
  <div>
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  beforeCreate() {
    console.log('beforeCreate: ', this.message); // 输出:undefined
  },
  created() {
    console.log('created: ', this.message); // 输出:Hello, Vue!
  }
};
</script>
  • 挂载阶段
    • beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。此时,虚拟 DOM 已经创建完成,但是还没有真正挂载到 DOM 上。
    • mounted:实例被挂载后调用,这时 el 被新创建的 vm.$el 替换,并挂载到实例上去了。如果根实例挂载到了一个文档内的元素上,当 mounted 被调用时,这个元素也会被插入到 DOM 中。
<template>
  <div ref="myDiv">
    <p>{{ message }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  beforeMount() {
    console.log('beforeMount: ', this.$refs.myDiv); // 输出:undefined
  },
  mounted() {
    console.log('mounted: ', this.$refs.myDiv); // 输出:<div ref="myDiv"><p>Hello, Vue!</p></div>
  }
};
</script>
  • 更新阶段
    • beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。此时,可以在这个钩子函数中获取更新前的状态。
    • updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。但是在这个钩子函数中,不要尝试对数据进行更改,这可能会导致无限循环。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  methods: {
    updateMessage() {
      this.message = 'Updated Message';
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate: ', this.message);
  },
  updated() {
    console.log('updated: ', this.message);
  }
};
</script>
  • 销毁阶段
    • beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用。可以在这个钩子函数中进行一些清理工作,比如清除定时器、解绑事件等。
    • destroyed:Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="destroyComponent">Destroy Component</button>
  </div>
</template>

<script>
export default {
  data() {
    return {
      message: 'Hello, Vue!'
    };
  },
  methods: {
    destroyComponent() {
      this.$destroy();
    }
  },
  beforeDestroy() {
    console.log('beforeDestroy: ', this.message);
  },
  destroyed() {
    console.log('destroyed: Component has been destroyed');
  }
};
</script>

生命周期优化的重要性

在大型 Vue 应用中,组件数量众多,每个组件的生命周期管理不当可能会导致性能问题。例如,不必要的更新会触发 beforeUpdateupdated 钩子函数,浪费资源。如果在 mounted 钩子函数中执行了大量的 DOM 操作或者异步请求,可能会导致页面渲染卡顿。合理优化组件生命周期,可以有效提升应用的性能和用户体验。

减少不必要的更新

  1. 使用 Object.freeze 冻结数据 Vue 通过数据劫持来实现数据响应式,当数据发生变化时,会触发组件的更新。如果某些数据在组件的生命周期中不会发生变化,可以使用 Object.freeze 冻结该数据,这样 Vue 就不会对其进行响应式追踪,从而避免不必要的更新。
<template>
  <div>
    <p>{{ staticData }}</p>
  </div>
</template>

<script>
export default {
  data() {
    const staticData = {
      title: 'This is a static title'
    };
    return {
      staticData: Object.freeze(staticData)
    };
  }
};
</script>
  1. 计算属性缓存 计算属性是基于它们的依赖进行缓存的。只有在它的依赖数据发生变化时才会重新求值。相比方法调用,计算属性在依赖不变的情况下不会重复计算,从而减少不必要的更新。
<template>
  <div>
    <p>{{ fullName }}</p>
    <input v-model="firstName">
    <input v-model="lastName">
  </div>
</template>

<script>
export default {
  data() {
    return {
      firstName: 'John',
      lastName: 'Doe'
    };
  },
  computed: {
    fullName() {
      return this.firstName + ' ' + this.lastName;
    }
  }
};
</script>
  1. shouldUpdate 函数优化 在 Vue 2.x 中,可以通过使用 Vue.mixin 来创建一个全局的 shouldUpdate 函数,用于控制组件是否需要更新。在 Vue 3.x 中,可以使用 watchEffect 配合 shallowRef 等实现类似的功能。
<template>
  <div>
    <p>{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
  </div>
</template>

<script>
// Vue 2.x 示例
// import Vue from 'vue';
// Vue.mixin({
//   beforeUpdate() {
//     const shouldUpdate = this.shouldUpdate();
//     if (!shouldUpdate) {
//       return false;
//     }
//   }
// });

// Vue 3.x 示例
import { watchEffect, shallowRef } from 'vue';

export default {
  setup() {
    const message = shallowRef('Hello, Vue!');
    const shouldUpdate = shallowRef(true);

    const updateMessage = () => {
      // 假设某些条件下不更新
      if (Math.random() > 0.5) {
        shouldUpdate.value = false;
      } else {
        message.value = 'Updated Message';
      }
    };

    watchEffect(() => {
      if (shouldUpdate.value) {
        // 实际更新逻辑
      }
    });

    return {
      message,
      updateMessage
    };
  }
};
</script>

优化挂载阶段操作

  1. 异步组件加载 在组件挂载时,如果需要加载大量的数据或者执行复杂的初始化操作,可以使用异步组件加载。这样可以将这些操作推迟到组件需要渲染时才执行,避免阻塞页面的初始渲染。
<template>
  <div>
    <async-component></async-component>
  </div>
</template>

<script>
import { defineAsyncComponent } from 'vue';

const AsyncComponent = defineAsyncComponent(() => import('./AsyncComponent.vue'));

export default {
  components: {
    asyncComponent: AsyncComponent
  }
};
</script>
  1. 防抖和节流mounted 钩子函数中,如果需要绑定一些事件监听器,这些事件可能会频繁触发,比如滚动事件、窗口大小改变事件等。使用防抖和节流技术可以有效减少事件触发的频率,提升性能。
<template>
  <div>
    <p>{{ scrollY }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      scrollY: 0
    };
  },
  mounted() {
    const debounce = (func, delay) => {
      let timer;
      return function() {
        const context = this;
        const args = arguments;
        clearTimeout(timer);
        timer = setTimeout(() => {
          func.apply(context, args);
        }, delay);
      };
    };

    const handleScroll = () => {
      this.scrollY = window.pageYOffset;
    };

    window.addEventListener('scroll', debounce(handleScroll, 200));
  }
};
</script>

合理使用销毁阶段钩子

  1. 清除定时器 如果在组件的生命周期中设置了定时器,在组件销毁时需要清除定时器,避免内存泄漏。
<template>
  <div>
    <p>{{ count }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      count: 0,
      timer: null
    };
  },
  mounted() {
    this.timer = setInterval(() => {
      this.count++;
    }, 1000);
  },
  beforeDestroy() {
    clearInterval(this.timer);
  }
};
</script>
  1. 解绑自定义事件 如果在组件中使用了 $on 绑定了自定义事件,在组件销毁时需要使用 $off 解绑这些事件。
<template>
  <div>
    <button @click="sendCustomEvent">Send Custom Event</button>
  </div>
</template>

<script>
export default {
  methods: {
    sendCustomEvent() {
      this.$emit('custom-event', 'Hello from component');
    }
  },
  mounted() {
    this.$on('custom-event', (message) => {
      console.log(message);
    });
  },
  beforeDestroy() {
    this.$off('custom-event');
  }
};
</script>

父子组件生命周期协调

  1. 父子组件挂载顺序 父组件的 beforeCreatecreatedbeforeMount 钩子函数会先于子组件执行,然后子组件依次执行 beforeCreatecreatedbeforeMountmounted,最后父组件执行 mounted。了解这个顺序对于在合适的时机进行初始化操作很重要。
<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component</p>
    <ChildComponent></ChildComponent>
  </div>
</template>

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

export default {
  components: {
    ChildComponent
  },
  beforeCreate() {
    console.log('Parent beforeCreate');
  },
  created() {
    console.log('Parent created');
  },
  beforeMount() {
    console.log('Parent beforeMount');
  },
  mounted() {
    console.log('Parent mounted');
  }
};
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
    <p>Child Component</p>
  </div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('Child beforeCreate');
  },
  created() {
    console.log('Child created');
  },
  beforeMount() {
    console.log('Child beforeMount');
  },
  mounted() {
    console.log('Child mounted');
  }
};
</script>
  1. 父子组件更新顺序 当父组件的数据发生变化时,父组件会先触发 beforeUpdate 钩子函数,然后子组件依次触发 beforeUpdate,子组件更新完成后触发 updated,最后父组件触发 updated
<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component: {{ parentData }}</p>
    <button @click="updateParentData">Update Parent Data</button>
    <ChildComponent :childData="parentData"></ChildComponent>
  </div>
</template>

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

export default {
  components: {
    ChildComponent
  },
  data() {
    return {
      parentData: 'Initial data'
    };
  },
  methods: {
    updateParentData() {
      this.parentData = 'Updated data';
    }
  },
  beforeUpdate() {
    console.log('Parent beforeUpdate');
  },
  updated() {
    console.log('Parent updated');
  }
};
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
    <p>Child Component: {{ childData }}</p>
  </div>
</template>

<script>
export default {
  props: ['childData'],
  beforeUpdate() {
    console.log('Child beforeUpdate');
  },
  updated() {
    console.log('Child updated');
  }
};
</script>
  1. 父子组件销毁顺序 父组件的 beforeDestroy 钩子函数会先执行,然后子组件依次执行 beforeDestroydestroyed,最后父组件执行 destroyed。在销毁过程中,要确保子组件的资源先被清理,再清理父组件的资源。
<!-- ParentComponent.vue -->
<template>
  <div>
    <p>Parent Component</p>
    <ChildComponent></ChildComponent>
    <button @click="destroyParentComponent">Destroy Parent Component</button>
  </div>
</template>

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

export default {
  components: {
    ChildComponent
  },
  methods: {
    destroyParentComponent() {
      this.$destroy();
    }
  },
  beforeDestroy() {
    console.log('Parent beforeDestroy');
  },
  destroyed() {
    console.log('Parent destroyed');
  }
};
</script>

<!-- ChildComponent.vue -->
<template>
  <div>
    <p>Child Component</p>
  </div>
</template>

<script>
export default {
  beforeDestroy() {
    console.log('Child beforeDestroy');
  },
  destroyed() {
    console.log('Child destroyed');
  }
};
</script>

错误处理与生命周期

  1. 捕获生命周期钩子中的错误 在生命周期钩子函数中,如果发生错误,可能会导致组件异常甚至应用崩溃。可以使用 try...catch 语句来捕获错误,并进行相应的处理。
<template>
  <div>
    <p>{{ data }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      data: null
    };
  },
  mounted() {
    try {
      // 模拟一个可能出错的操作
      this.data = JSON.parse('{invalid json');
    } catch (error) {
      console.error('Error in mounted hook: ', error);
      this.data = 'Error occurred';
    }
  }
};
</script>
  1. 全局错误处理 在 Vue 应用中,可以通过 app.config.errorHandler(Vue 3)或 Vue.config.errorHandler(Vue 2)来设置全局的错误处理函数,捕获组件生命周期钩子以及 Promise 拒绝等未处理的错误。
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Vue Error Handling</title>
  <script src="https://unpkg.com/vue@3.2.37/dist/vue.global.prod.js"></script>
</head>

<body>
  <div id="app">
    <MyComponent></MyComponent>
  </div>
  <script>
    const MyComponent = {
      template: `<div><p>{{ data }}</p></div>`,
      data() {
        return {
          data: null
        };
      },
      mounted() {
        // 模拟一个可能出错的操作
        this.data = JSON.parse('{invalid json');
      }
    };

    const app = Vue.createApp({
      components: {
        MyComponent
      }
    });

    app.config.errorHandler = (error, instance, info) => {
      console.error('Global error handler: ', error, 'in instance: ', instance, 'info: ', info);
    };

    app.mount('#app');
  </script>
</body>

</html>

通过合理运用以上 Vue 组件生命周期的优化策略,可以有效提升 Vue 应用的性能、稳定性和可维护性。在实际开发中,需要根据具体的业务场景和需求,灵活选择和组合这些优化方法,以达到最佳的效果。