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

Vue生命周期钩子 动态加载组件时的钩子触发顺序

2023-11-104.8k 阅读

Vue 生命周期钩子概述

在深入探讨动态加载组件时钩子的触发顺序之前,先来回顾一下 Vue 生命周期钩子的基本概念。Vue 实例从创建到销毁的过程,就是生命周期。在这个过程中,Vue 提供了一系列的生命周期钩子函数,让开发者可以在特定的阶段执行自定义的逻辑。

常用的生命周期钩子

  1. beforeCreate:在实例初始化之后,数据观测(data observer)和 event/watcher 事件配置之前被调用。此时,实例上的数据和方法都还未初始化,this 指向的是一个空的 Vue 实例。例如:
new Vue({
  beforeCreate() {
    console.log('beforeCreate 钩子被调用,此时 data 中的数据还未初始化:', this.message);
  },
  data() {
    return {
      message: 'Hello, Vue!'
    };
  }
});

在上述代码中,beforeCreate 钩子中试图访问 this.message,会得到 undefined

  1. created:实例已经创建完成,数据观测和 event/watcher 事件配置已完成,但尚未挂载到 DOM 上。此时可以访问 data 中的数据和 methods 中的方法。例如:
new Vue({
  created() {
    console.log('created 钩子被调用,此时可以访问 data 中的数据:', this.message);
  },
  data() {
    return {
      message: 'Hello, Vue!'
    };
  }
});

created 钩子中,可以正常访问 this.message

  1. beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。此时,$el 还未被创建,但 template 模板已经编译完成,即将被挂载到 DOM 上。例如:
<div id="app">
  {{ message }}
</div>
<script>
new Vue({
  el: '#app',
  beforeMount() {
    console.log('beforeMount 钩子被调用,此时 $el 还未挂载到 DOM 上:', this.$el);
  },
  data() {
    return {
      message: 'Hello, Vue!'
    };
  }
});
</script>

beforeMount 钩子中,this.$elnull

  1. mounted:实例被挂载到 DOM 上后调用。此时,可以通过 this.$el 访问到真实的 DOM 元素。例如:
<div id="app">
  {{ message }}
</div>
<script>
new Vue({
  el: '#app',
  mounted() {
    console.log('mounted 钩子被调用,此时 $el 已挂载到 DOM 上:', this.$el);
  },
  data() {
    return {
      message: 'Hello, Vue!'
    };
  }
});
</script>

mounted 钩子中,this.$el 指向 <div id="app">Hello, Vue!</div>

  1. beforeUpdate:数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。可以在这个钩子中获取更新前的状态。例如:
<div id="app">
  <button @click="updateMessage">更新消息</button>
  <p>{{ message }}</p>
</div>
<script>
new Vue({
  el: '#app',
  data() {
    return {
      message: '初始消息'
    };
  },
  methods: {
    updateMessage() {
      this.message = '更新后的消息';
    }
  },
  beforeUpdate() {
    console.log('beforeUpdate 钩子被调用,更新前的消息:', this.message);
  }
});
</script>

当点击按钮更新 message 时,beforeUpdate 钩子会在 DOM 更新之前被调用。

  1. updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经更新完成。例如:
<div id="app">
  <button @click="updateMessage">更新消息</button>
  <p>{{ message }}</p>
</div>
<script>
new Vue({
  el: '#app',
  data() {
    return {
      message: '初始消息'
    };
  },
  methods: {
    updateMessage() {
      this.message = '更新后的消息';
    }
  },
  updated() {
    console.log('updated 钩子被调用,DOM 已更新完成');
  }
});
</script>

updated 钩子中,可以确保 DOM 已经更新为新的数据状态。

  1. beforeDestroy:实例销毁之前调用。在这一步,实例仍然完全可用,可以在这里清理定时器、解绑事件等。例如:
<div id="app">
  <button @click="destroyInstance">销毁实例</button>
</div>
<script>
new Vue({
  el: '#app',
  data() {
    return {
      timer: null
    };
  },
  created() {
    this.timer = setInterval(() => {
      console.log('定时器在运行');
    }, 1000);
  },
  methods: {
    destroyInstance() {
      this.$destroy();
    }
  },
  beforeDestroy() {
    clearInterval(this.timer);
    console.log('beforeDestroy 钩子被调用,定时器已清理');
  }
});
</script>

beforeDestroy 钩子中清理定时器,防止内存泄漏。

  1. destroyed:实例销毁后调用。此时,所有的指令已解绑,所有的事件监听器已移除,所有的子实例也已被销毁。例如:
<div id="app">
  <button @click="destroyInstance">销毁实例</button>
</div>
<script>
new Vue({
  el: '#app',
  methods: {
    destroyInstance() {
      this.$destroy();
    }
  },
  destroyed() {
    console.log('destroyed 钩子被调用,实例已销毁');
  }
});
</script>

destroyed 钩子中,实例已经处于销毁状态。

动态加载组件

动态组件的概念

动态组件是指在 Vue 应用中,根据不同的条件渲染不同的组件。这在很多场景下非常有用,比如在单页应用中实现多视图切换,或者根据用户角色显示不同的界面等。Vue 提供了 <component> 元素,并结合 is 特性来实现动态组件。例如:

<template>
  <div>
    <button @click="changeComponent">切换组件</button>
    <component :is="currentComponent"></component>
  </div>
</template>

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

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

在上述代码中,通过点击按钮切换 currentComponent 的值,从而动态地渲染 ComponentAComponentB

动态加载组件的方式

  1. 使用 import() 动态导入组件:ES2020 引入了动态导入模块的语法 import(),Vue 中可以利用它来实现组件的按需加载,减少初始加载的体积。例如:
<template>
  <div>
    <button @click="loadComponent">加载组件</button>
    <component v-if="loadedComponent" :is="loadedComponent"></component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      loadedComponent: null
    };
  },
  methods: {
    loadComponent() {
      import('./AsyncComponent.vue')
      .then(component => {
        this.loadedComponent = component.default;
      });
    }
  }
};
</script>

在上述代码中,点击按钮时通过 import() 动态导入 AsyncComponent.vue 组件,并将其赋值给 loadedComponent 进行渲染。

  1. 使用 Vue.component() 动态注册组件:在 Vue 实例内部,可以使用 Vue.component() 方法动态注册组件。例如:
<template>
  <div>
    <button @click="registerAndLoadComponent">注册并加载组件</button>
    <component v-if="registeredComponent" :is="registeredComponent"></component>
  </div>
</template>

<script>
export default {
  data() {
    return {
      registeredComponent: null
    };
  },
  methods: {
    registerAndLoadComponent() {
      const DynamicComponent = {
        template: '<div>动态注册的组件</div>'
      };
      Vue.component('DynamicComponent', DynamicComponent);
      this.registeredComponent = 'DynamicComponent';
    }
  }
};
</script>

在上述代码中,点击按钮时动态注册 DynamicComponent 组件,并将其渲染到页面上。

动态加载组件时钩子触发顺序

首次加载动态组件

  1. 父组件钩子:当父组件中动态加载一个组件时,首先触发父组件的 beforeCreatecreated 钩子。这是因为父组件需要先完成自身的初始化,包括数据观测和事件配置等。例如:
<template>
  <div>
    <button @click="loadDynamicComponent">加载动态组件</button>
    <component v-if="dynamicComponent" :is="dynamicComponent"></component>
  </div>
</template>

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

export default {
  data() {
    return {
      dynamicComponent: null
    };
  },
  methods: {
    loadDynamicComponent() {
      this.dynamicComponent = DynamicComponent;
    }
  },
  beforeCreate() {
    console.log('父组件 beforeCreate 钩子');
  },
  created() {
    console.log('父组件 created 钩子');
  }
};
</script>
  1. 子组件钩子:接着,触发子组件(动态加载的组件)的 beforeCreatecreated 钩子。因为子组件在被渲染之前也需要进行自身的初始化。例如,在 DynamicComponent.vue 中:
<template>
  <div>动态组件内容</div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('动态组件 beforeCreate 钩子');
  },
  created() {
    console.log('动态组件 created 钩子');
  }
};
</script>
  1. 子组件挂载钩子:然后,子组件触发 beforeMountmounted 钩子。此时子组件的模板已经编译完成,即将被挂载到 DOM 上,并且最终完成挂载。例如:
<template>
  <div>动态组件内容</div>
</template>

<script>
export default {
  beforeMount() {
    console.log('动态组件 beforeMount 钩子');
  },
  mounted() {
    console.log('动态组件 mounted 钩子');
  }
};
</script>
  1. 父组件挂载钩子:最后,父组件触发 beforeMountmounted 钩子。因为父组件需要在子组件挂载完成后,将包含子组件的整个 DOM 结构挂载到页面上。例如:
<template>
  <div>
    <button @click="loadDynamicComponent">加载动态组件</button>
    <component v-if="dynamicComponent" :is="dynamicComponent"></component>
  </div>
</template>

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

export default {
  data() {
    return {
      dynamicComponent: null
    };
  },
  methods: {
    loadDynamicComponent() {
      this.dynamicComponent = DynamicComponent;
    }
  },
  beforeMount() {
    console.log('父组件 beforeMount 钩子');
  },
  mounted() {
    console.log('父组件 mounted 钩子');
  }
};
</script>

所以,首次加载动态组件时,钩子触发顺序为:父组件 beforeCreate -> 父组件 created -> 子组件 beforeCreate -> 子组件 created -> 子组件 beforeMount -> 子组件 mounted -> 父组件 beforeMount -> 父组件 mounted

动态切换组件

  1. 旧组件销毁钩子:当动态切换组件时,首先触发旧组件的 beforeDestroy 钩子。此时旧组件仍然可用,可以在这个钩子中进行清理工作,比如清理定时器、解绑事件等。例如,假设当前显示 ComponentA,要切换到 ComponentB
<template>
  <div>
    <button @click="switchComponent">切换组件</button>
    <component :is="currentComponent"></component>
  </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>组件 A 的内容</div>
</template>

<script>
export default {
  beforeDestroy() {
    console.log('组件 A 的 beforeDestroy 钩子');
  }
};
</script>
  1. 旧组件销毁完成:接着,旧组件触发 destroyed 钩子,表示旧组件已经完全销毁,所有的指令已解绑,事件监听器已移除。例如:
<template>
  <div>组件 A 的内容</div>
</template>

<script>
export default {
  destroyed() {
    console.log('组件 A 的 destroyed 钩子');
  }
};
</script>
  1. 新组件创建和挂载钩子:然后,新组件触发 beforeCreatecreatedbeforeMountmounted 钩子,其过程与首次加载动态组件时新组件的创建和挂载过程相同。例如,在 ComponentB.vue 中:
<template>
  <div>组件 B 的内容</div>
</template>

<script>
export default {
  beforeCreate() {
    console.log('组件 B 的 beforeCreate 钩子');
  },
  created() {
    console.log('组件 B 的 created 钩子');
  },
  beforeMount() {
    console.log('组件 B 的 beforeMount 钩子');
  },
  mounted() {
    console.log('组件 B 的 mounted 钩子');
  }
};
</script>

所以,动态切换组件时,钩子触发顺序为:旧组件 beforeDestroy -> 旧组件 destroyed -> 新组件 beforeCreate -> 新组件 created -> 新组件 beforeMount -> 新组件 mounted

动态加载组件更新时钩子触发顺序

  1. 父组件更新钩子:当动态加载组件的数据发生变化导致更新时,首先触发父组件的 beforeUpdate 钩子。例如:
<template>
  <div>
    <button @click="updateData">更新数据</button>
    <component :is="dynamicComponent"></component>
  </div>
</template>

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

export default {
  data() {
    return {
      dynamicComponent: DynamicComponent,
      parentData: '初始数据'
    };
  },
  methods: {
    updateData() {
      this.parentData = '更新后的数据';
    }
  },
  beforeUpdate() {
    console.log('父组件 beforeUpdate 钩子');
  }
};
</script>
  1. 子组件更新钩子:接着,触发子组件的 beforeUpdate 钩子。子组件也会检测到数据的变化,准备进行更新。例如,在 DynamicComponent.vue 中:
<template>
  <div>{{ childData }}</div>
</template>

<script>
export default {
  props: ['childData'],
  beforeUpdate() {
    console.log('子组件 beforeUpdate 钩子');
  }
};
</script>
  1. 子组件更新完成钩子:然后,子组件触发 updated 钩子,表示子组件的更新已经完成,DOM 已经更新为新的数据状态。例如:
<template>
  <div>{{ childData }}</div>
</template>

<script>
export default {
  props: ['childData'],
  updated() {
    console.log('子组件 updated 钩子');
  }
};
</script>
  1. 父组件更新完成钩子:最后,父组件触发 updated 钩子,表示父组件的更新也已完成。例如:
<template>
  <div>
    <button @click="updateData">更新数据</button>
    <component :is="dynamicComponent"></component>
  </div>
</template>

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

export default {
  data() {
    return {
      dynamicComponent: DynamicComponent,
      parentData: '初始数据'
    };
  },
  methods: {
    updateData() {
      this.parentData = '更新后的数据';
    }
  },
  updated() {
    console.log('父组件 updated 钩子');
  }
};
</script>

所以,动态加载组件更新时,钩子触发顺序为:父组件 beforeUpdate -> 子组件 beforeUpdate -> 子组件 updated -> 父组件 updated

应用场景及注意事项

应用场景

  1. 单页应用路由切换:在单页应用(SPA)中,使用 Vue Router 进行路由切换时,本质上就是动态加载不同的组件。了解钩子触发顺序有助于在路由切换时进行数据的预加载、页面状态的保存和恢复等操作。例如,在进入新路由对应的组件前,可以在 beforeCreatecreated 钩子中发起 API 请求获取数据,在离开当前路由对应的组件时,可以在 beforeDestroy 钩子中清理定时器等资源。
  2. 动态表单组件:在一些复杂的表单场景中,可能需要根据用户的选择动态加载不同的表单组件。比如,一个注册表单,当用户选择注册类型为“个人”或“企业”时,加载不同的表单字段组件。通过掌握钩子触发顺序,可以在组件加载和切换时进行表单数据的校验、初始化等操作。

注意事项

  1. 内存泄漏问题:在动态加载和切换组件时,要注意在 beforeDestroy 钩子中清理所有的定时器、事件监听器等可能导致内存泄漏的资源。如果不清理,随着组件的频繁切换,可能会导致内存占用不断增加,最终影响应用的性能。
  2. 数据传递和同步:动态加载组件时,要确保父组件和子组件之间的数据传递和同步正常。在组件创建和更新时,要根据钩子触发顺序合理地处理数据的初始化和更新,避免出现数据不一致的情况。例如,在父组件更新数据后,要确保子组件能够正确地接收到更新后的数据并进行相应的渲染。
  3. 性能优化:虽然动态加载组件可以实现按需加载,提高应用的性能,但如果组件切换过于频繁,可能会导致性能下降。在这种情况下,可以考虑使用缓存机制,比如 keep - alive 组件,来缓存动态组件的状态,避免重复创建和销毁组件,从而提高性能。例如:
<template>
  <div>
    <button @click="switchComponent">切换组件</button>
    <keep - alive>
      <component :is="currentComponent"></component>
    </keep - alive>
  </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>

在上述代码中,使用 keep - alive 包裹动态组件,当组件切换时,被切换掉的组件不会被销毁,而是被缓存起来,再次切换回来时可以直接使用缓存的状态,从而提高性能。

通过深入理解 Vue 生命周期钩子在动态加载组件时的触发顺序,可以更好地编写健壮、高效的 Vue 应用程序,避免潜在的问题,并充分发挥 Vue 的优势。无论是在小型项目还是大型企业级应用中,掌握这些知识都将对开发工作带来很大的帮助。在实际开发中,应根据具体的业务需求,合理地利用生命周期钩子,实现组件的灵活加载、切换和更新,提升用户体验和应用性能。