Vue3 组件生命周期详解

在上一篇文章中,我们深入探讨了组件从VNode到DOM的渲染过程。本篇文章将聚焦于组件的生命周期------这个贯穿组件从创建到销毁整个过程的时间轴。理解生命周期,不仅能帮助我们写出更可靠的代码,还能在合适的时机做合适的事情。

前言:为什么需要生命周期?

想象一下,我们正在搭建一座房子,一般需要经过以下几个阶段:

  1. 创建阶段:设计图纸、准备材料
  2. 挂载阶段:打地基、砌墙、安装门窗
  3. 更新阶段:翻新墙面、更换家具
  4. 卸载阶段:拆除房屋、清理场地

组件也是如此。在它的整个生命周期中,我们需要在不同的时间点执行不同的操作:

javascript 复制代码
const Component = {
  // 创建时:初始化数据
  created() {
    this.fetchData();
  },
  
  // 挂载后:操作DOM
  mounted() {
    this.$el.focus();
  },

  // 更新时:页面刷新
  updated() {
    this.$el.scrollTop = this.$el.scrollHeight; 		
  }
  
  // 卸载前:清理资源
  beforeUnmount() {
    clearInterval(this.timer);
  }
};

生命周期的完整图谱

生命周期全景图

下面这张图展示了 Vue3 组件的完整生命周期流程:

各个阶段的核心任务

阶段 钩子 核心任务 注意事项
创建阶段 setup / beforeCreate / created 初始化数据、设置响应式 无法访问DOM
挂载阶段 beforeMount / mounted 渲染DOM、操作DOM 可以访问DOM
更新阶段 beforeUpdate / updated 响应数据变化 避免在更新钩子中修改数据
卸载阶段 beforeUnmount / unmounted 清理资源 清除定时器、取消订阅
缓存阶段 activated / deactivated 配合keep-alive 缓存组件的激活/失活

创建阶段:组件诞生

创建阶段的三个钩子

在 Vue3 中,创建阶段实际上由三个关键步骤组成:

  1. 最先执行:setup
  2. 然后执行:beforeCreate
  3. 最后执行:created
javascript 复制代码
export default {
  // 1. 最先执行:setup
  setup() {
    console.log('1. setup 执行');
    
    const count = ref(0);
    
    // setup 中不能使用 this
    // console.log(this); // undefined
    
    return { count };
  },
  
  // 2. 然后执行:beforeCreate
  beforeCreate() {
    console.log('2. beforeCreate 执行');
    console.log('数据尚未初始化:', this.count); // undefined
    console.log('DOM 尚未创建:', this.$el);     // undefined
  },
  
  // 3. 最后执行:created
  created() {
    console.log('3. created 执行');
    console.log('数据已初始化:', this.count);    // 0
    console.log('DOM 尚未创建:', this.$el);      // undefined
  }
};

各钩子的数据访问能力

为了更好地理解每个阶段能做什么,我们用一个表格来展示:

钩子 访问 data 访问 props 访问 computed 访问 methods 访问 DOM 访问 $el
setup ❌ (尚未创建) ❌ (尚未创建)
beforeCreate
created

为什么需要三个创建钩子?

你可能有这样的疑问:为什么有了 setup 还要保留 beforeCreatecreated

这其实是为了兼容性和渐进迁移。在 Vue3 中,setup 实际上是 beforeCreatecreated 的替代品;但是 Vue3 为了向下兼容 Vue2 ,仍然保留了 beforeCreatecreated

Vue2 风格的创建钩子:

javascript 复制代码
export default {
  beforeCreate() {
    // 初始化非响应式数据
    this.nonReactive = {};
  },
  created() {
    // 发起API请求
    this.fetchData();
  }
};

Vue3 组合式风格:

javascript 复制代码
export default {
  setup() {
    // 初始化非响应式数据
    const nonReactive = {};
    
    // 发起API请求
    fetchData();
    
    return { nonReactive };
  }
};

挂载阶段:组件展现

挂载过程的内部机制

挂载阶段是组件第一次将虚拟 DOM 渲染为真实 DOM 的过程:

javascript 复制代码
export default {
  beforeMount() {
    console.log('1. beforeMount 执行');
    console.log('此时已有编译好的模板,但尚未挂载到DOM');
    console.log('DOM 尚不存在:', this.$el);  // undefined
  },
  
  // render 函数在 beforeMount 之后、mounted 之前执行
  render() {
    console.log('2. render 执行');
    return h('div', 'Hello World');
  },
  
  mounted() {
    console.log('3. mounted 执行');
    console.log('DOM 已创建并挂载:', this.$el);     // <div>Hello World</div>
    console.log('可以安全地操作DOM了');
    
    // 可以访问DOM元素
    this.$el.querySelector('input')?.focus();
    
    // 可以集成第三方库
    new Chart(this.$el.querySelector('#chart'), {...});
  }
};

挂载阶段的时序图

挂载阶段的典型应用场景

1. 操作DOM

javascript 复制代码
this.$el.scrollTop = 100;

2. 获取元素尺寸

javascript 复制代码
const width = this.$refs.box.offsetWidth;

3. 集成第三方库(需要DOM存在)

javascript 复制代码
this.chart = new Chart(this.$refs.canvas, {
  type: 'line',
  data: this.chartData
});

4. 添加全局事件监听

javascript 复制代码
window.addEventListener('resize', this.handleResize);

5. 启动定时器

javascript 复制代码
this.timer = setInterval(this.refreshData, 5000);

6. 卸载前清理操作

javascript 复制代码
window.removeEventListener('resize', this.handleResize);
clearInterval(this.timer);
this.chart?.destroy();

更新阶段:组件响应

更新阶段的时序图

更新阶段的注意事项

javascript 复制代码
export default {
  beforeUpdate() {
    // ✅ 可以在DOM更新前访问旧状态
    const oldHeight = this.$refs.box.offsetHeight;
    console.log('旧高度:', oldHeight);
    
    // ❌ 不要在更新钩子中修改数据(可能造成死循环)
    // this.count++; // 会触发无限循环
    
    // ✅ 可以在这里手动操作DOM(不推荐)
    // 但要注意这些操作可能会被后续的更新覆盖
  },
  
  updated() {
    // ✅ 可以获取更新后的DOM信息
    const newHeight = this.$refs.box.offsetHeight;
    console.log('新高度:', newHeight);
    
    // ✅ 可以根据新状态调整其他非响应式内容
    if (newHeight > 500) {
      this.$refs.box.classList.add('overflow');
    }
    
    // ❌ 同样避免在这里修改数据
    // ❌ 避免直接操作DOM来"修复"样式,应该通过数据驱动
  }
};

卸载阶段:组件消亡

卸载的完整过程

javascript 复制代码
export default {
  beforeUnmount() {
    console.log('1. beforeUnmount 执行');
    console.log('组件即将被卸载,但依然可以访问');
    console.log('DOM 仍然存在:', this.$el);
    
    // 清理工作
    this.cleanup();
  },
  
  unmounted() {
    console.log('2. unmounted 执行');
    console.log('组件已被卸载');
    console.log('DOM 已移除:', this.$el); // 被移除或置空
    
    // 最终清理
    this.finalCleanup();
  }
};

需要清理的典型资源

  1. 定时器:clearInterval(timer);
  2. 事件监听:window.removeEventListener('resize', handleResize);
  3. 观察者:observer.disconnect();
  4. 网络请求:controller.abort();
  5. 第三方库实例:chart.destroy();
  6. 手动订阅:subscription.unsubscribe();

缓存阶段:KeepAlive 的特殊生命周期

为什么需要缓存阶段?

当组件被 <KeepAlive> 包裹时,它的生命周期会发生变化:

activated 和 deactivated 的使用

javascript 复制代码
const CacheComponent = {
  setup() {
    console.log('setup 执行'); // 只执行一次
    
    const count = ref(0);
    
    // 这些钩子会在组件被缓存时特殊处理
    onMounted(() => {
      console.log('mounted 执行'); // 只执行一次
    });
    
    onActivated(() => {
      console.log('activated 执行'); // 每次进入视图时执行
      
      // 恢复一些状态
      startAnimation();
      startPolling();
    });
    
    onDeactivated(() => {
      console.log('deactivated 执行'); // 每次离开视图时执行
      
      // 暂停一些操作,但不销毁
      stopAnimation();
      stopPolling();
    });
    
    onUnmounted(() => {
      console.log('unmounted 执行'); // 最终销毁时执行
    });
    
    return { count };
  }
};

父子组件的生命周期顺序

挂载阶段的执行顺序

当父子组件嵌套时,生命周期的执行顺序非常关键:

javascript 复制代码
const Parent = {
  setup() { console.log('Parent setup'); },
  beforeCreate() { console.log('Parent beforeCreate'); },
  created() { console.log('Parent created'); },
  beforeMount() { console.log('Parent beforeMount'); },
  mounted() { console.log('Parent mounted'); },
  
  render() {
    return h('div', [
      h(Child)
    ]);
  }
};

const Child = {
  setup() { console.log('Child setup'); },
  beforeCreate() { console.log('Child beforeCreate'); },
  created() { console.log('Child created'); },
  beforeMount() { console.log('Child beforeMount'); },
  mounted() { console.log('Child mounted'); }
};

// 渲染输出顺序:
// 1. Parent setup
// 2. Parent beforeCreate
// 3. Parent created
// 4. Parent beforeMount
// 5. Child setup
// 6. Child beforeCreate
// 7. Child created
// 8. Child beforeMount
// 9. Child mounted
// 10. Parent mounted

更新阶段的执行顺序

当 Parent 的数据变化时:

  1. Parent beforeUpdate
  2. Child beforeUpdate
  3. Child updated
  4. Parent updated

卸载阶段的执行顺序

当父组件被移除时:

  1. Parent beforeUnmount
  2. Child beforeUnmount
  3. Child unmounted
  4. Parent unmounted

执行顺序的规律总结

阶段 执行顺序 原因
创建 父 → 子 父组件先创建,才能传递props给子组件
挂载 子 → 父 父组件需要等待所有子组件挂载完成
更新 父 → 子 父组件数据变化,传递给子组件
卸载 子 → 父 先拆除内部,再拆除外部

Vue3 中两种写法的生命周期对比

Vue3 同时支持两种写法:选项式 API组合式 API

选项式 API 生命周期

  • beforeCreate / created
  • beforeMount / mounted
  • beforeUpdate / updated
  • beforeUnmount / unmounted
  • activated / deactivated
  • errorCaptured
  • renderTracked / renderTriggered:新增的调试钩子

组合式 API 生命周期

  • setup
  • onBeforeMount / onMounted
  • onBeforeUpdate / onUpdated
  • onBeforeUnmount / onUnmounted
  • onActivated / onDeactivated
  • onErrorCaptured
  • onRenderTracked / onRenderTriggered

两种写法的对应关系表

选项式 API 组合式 API 执行时机
beforeCreate/created 直接在 setup 中编写代码 组件初始化前/组件初始化后
beforeMount/mounted onBeforeMount/onMounted DOM 挂载前/DOM 挂载后
beforeUpdate/updated onBeforeUpdate/onUpdated 数据更新、DOM 更新前/DOM 更新后
beforeUnmount/unmounted onBeforeUnmount/onUnmounted 组件卸载前/组件卸载后
activated/deactivated onActivated/onDeactivated keep-alive 组件激活/keep-alive 组件失活
errorCaptured onErrorCaptured 捕获后代组件错误

核心差异:setup 中的生命周期

setup 函数是最早的生命周期钩子,本身执行在 beforeCreatecreated 之前,属于 beforeCreatecreated 的替代品,因此在 setup 中编写的代码相当于在这两个钩子中执行:

javascript 复制代码
export default {
  setup() {
    // 这些代码相当于在 beforeCreate 和 created 中执行
    
    console.log('相当于 beforeCreate/created');
    
    const count = ref(0);
    
    // 可以在这里执行初始化操作
    fetchData();
    
    // 注册生命周期钩子
    onMounted(() => {
      console.log('mounted');
    });
    
    return { count };
  }
};

<script setup> 的特殊性

<script setup> 的本质

<script setup> 是组合式 API 的语法糖,它在编译时会被转换为普通的 setup() 函数:

html 复制代码
<!-- 源码写法 -->
<script setup>
import { ref, onMounted } from 'vue';

const count = ref(0);

function increment() {
  count.value++;
}

onMounted(() => {
  console.log('组件已挂载');
});
</script>
javascript 复制代码
// 编译后
export default {
  setup() {
    const count = ref(0);
    
    function increment() {
      count.value++;
    }
    
    onMounted(() => {
      console.log('组件已挂载');
    });
    
    return { count, increment };
  }
};

<script setup> 中的生命周期变化

<script setup> 中,生命周期钩子的使用变得更加简洁:

  • onBeforeMount / onMounted
  • onBeforeUpdate / onUpdated
  • onBeforeUnmount / onUnmounted
  • onActivated / onDeactivated

<script setup> 的特殊特性

javascript 复制代码
<script setup>
// 1. 自动返回顶层变量
const count = ref(0);           // 自动暴露给模板
function increment() {}          // 自动暴露给模板

// 2. 支持顶层 await
const data = await fetchData();  // 组件会等待异步操作完成

// 3. 使用 defineProps 和 defineEmits
const props = defineProps({
  title: String
});

const emit = defineEmits(['update']);

// 4. 使用 defineExpose 暴露方法
defineExpose({
  resetCount: () => count.value = 0
});

// 5. 生命周期钩子可以直接使用
onMounted(() => {
  console.log('mounted');
});
</script>

生命周期的最佳实践

各阶段适合做什么

阶段 适合的操作 不适合的操作
setup/created 初始化数据、设置响应式、发起API请求 操作DOM、访问$el
beforeMount 最后一次数据修改机会 操作DOM
mounted 操作DOM、集成第三方库、添加事件监听 同步修改数据(可能触发额外更新)
beforeUpdate 访问更新前的DOM 修改数据(可能死循环)
updated 执行依赖更新后DOM的操作 修改数据(可能死循环)
beforeUnmount 清理资源、移除事件监听 异步操作
unmounted 最终清理 访问已销毁的实例

生命周期调试技巧

使用生命周期追踪

javascript 复制代码
<script setup>
import { onMounted, onUpdated, onRenderTracked, onRenderTriggered } from 'vue';

// 追踪渲染依赖
onRenderTracked((event) => {
  console.log('渲染依赖追踪:', event);
  // {
  //   key: 'count',      // 依赖的属性名
  //   target: {},        // 依赖的目标对象
  //   type: 'get',       // 操作类型
  // }
});

// 追踪渲染触发原因
onRenderTriggered((event) => {
  console.log('渲染触发原因:', event);
  // {
  //   key: 'count',
  //   target: {},
  //   type: 'set',
  //   oldValue: 0,
  //   newValue: 1
  // }
});

// 记录完整生命周期
onBeforeMount(() => console.log('🔄 beforeMount'));
onMounted(() => console.log('✅ mounted'));
onBeforeUpdate(() => console.log('🔄 beforeUpdate'));
onUpdated(() => console.log('✅ updated'));
onBeforeUnmount(() => console.log('🔄 beforeUnmount'));
onUnmounted(() => console.log('✅ unmounted'));
</script>

使用钩子组合

javascript 复制代码
// 创建可复用的生命周期逻辑
function useLogger(componentName) {
  onBeforeMount(() => {
    console.log(`${componentName} 准备挂载`);
  });
  
  onMounted(() => {
    console.log(`${componentName} 已挂载`);
  });
  
  onBeforeUnmount(() => {
    console.log(`${componentName} 准备卸载`);
  });
  
  onUnmounted(() => {
    console.log(`${componentName} 已卸载`);
  });
}

// 在组件中使用
<script setup>
const props = defineProps({ name: String });
useLogger(props.name);
</script>

结语

理解生命周期,就像是掌握了组件从生到死的完整剧本。知道在每个阶段该做什么、不该做什么,才能写出既高效又可靠的Vue应用。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!

相关推荐
wuhen_n1 小时前
渲染器核心:mount挂载过程
前端·javascript·vue.js
简离1 小时前
JS 函数参数默认值误区解析:传 null 为何不触发默认值?
前端
正儿八经蛙1 小时前
AI应用开发框架对比:LangChain vs. Semantic Kernel vs. DSPy 深度解析
前端
不想秃头的程序员2 小时前
vue3 Pinia 全解析:从入门到实战。
前端·javascript·vue.js
Mintopia2 小时前
提升 Canvas 2D 绘图技术:应对全面工业化场景的系统方法
前端
wuhen_n2 小时前
组件渲染:从组件到DOM
前端·javascript·vue.js
zhougl9962 小时前
Composition API 和 Options API
前端·javascript·vue.js
wuhen_n2 小时前
虚拟DOM:VNode的设计与创建
前端·javascript·vue.js
归叶再无青2 小时前
web服务安装部署、性能升级等(Apache、Nginx)
运维·前端·nginx·云原生·apache·bash