Skip to content

Vue 生命周期钩子详解

前言

Vue 组件的生命周期钩子是 Vue.js 框架的核心特性之一,它允许开发者在组件的不同阶段执行特定的逻辑。理解生命周期钩子对于编写高效、可维护的 Vue 应用至关重要。本文将详细讲解 Vue 2 和 Vue 3 的生命周期钩子,包括执行顺序、使用场景和常见问题。


一、生命周期概述

1.1 生命周期阶段

Vue 组件的生命周期分为三个主要阶段:

阶段说明包含钩子
创建阶段组件实例创建到挂载到 DOMbeforeCreate, created, beforeMount, mounted
更新阶段响应式数据变化导致重新渲染beforeUpdate, updated
销毁阶段组件实例从 DOM 移除并销毁beforeDestroy, destroyed

1.2 Vue 2 生命周期图示

┌─────────────────────────────────────────────────────────────┐
│                     Vue 实例创建                             │
└─────────────────────────────────────────────────────────────┘


                    ┌────────────────┐
                    │  beforeCreate  │  ← 实例创建前,数据未初始化
                    └────────────────┘


                    ┌────────────────┐
                    │    created     │  ← 实例创建完成,可访问 data、methods
                    └────────────────┘


                    ┌────────────────┐
                    │  beforeMount   │  ← DOM 挂载前,$el 未生成
                    └────────────────┘


              ┌──────────────────────────────┐
              │  渲染模板 → 生成虚拟 DOM      │
              │  → 创建真实 DOM               │
              └──────────────────────────────┘


                    ┌────────────────┐
                    │    mounted     │  ← DOM 挂载完成,可操作 DOM
                    └────────────────┘


          ┌──────────────────────────────────────┐
          │           数据变化触发更新              │
          └──────────────────────────────────────┘


                    ┌────────────────┐
                    │  beforeUpdate  │  ← 更新前,数据已变 DOM 未更
                    └────────────────┘


              ┌──────────────────────────────┐
              │  重新渲染模板 → 更新真实 DOM   │
              └──────────────────────────────┘


                    ┌────────────────┐
                    │    updated     │  ← 更新完成,DOM 已更新
                    └────────────────┘


          ┌──────────────────────────────────────┐
          │           组件销毁触发                │
          └──────────────────────────────────────┘


                    ┌────────────────┐
                    │ beforeDestroy  │  ← 销毁前,实例仍可访问
                    └────────────────┘


              ┌──────────────────────────────┐
              │  移除事件监听、清理定时器等     │
              └──────────────────────────────┘


                    ┌────────────────┐
                    │   destroyed    │  ← 销毁完成,实例被销毁
                    └────────────────┘

1.3 Vue 2 vs Vue 3 生命周期钩子对比

Vue 2 钩子Vue 3 选项式 APIVue 3 组合式 API说明
beforeCreatebeforeCreatesetup()实例创建前
createdcreatedsetup()实例创建完成
beforeMountbeforeMountonBeforeMountDOM 挂载前
mountedmountedonMountedDOM 挂载完成
beforeUpdatebeforeUpdateonBeforeUpdate更新前
updatedupdatedonUpdated更新完成
beforeDestroybeforeUnmountonBeforeUnmount销毁前
destroyedunmountedonUnmounted销毁完成
activatedactivatedonActivatedkeep-alive 激活
deactivateddeactivatedonDeactivatedkeep-alive 失活
errorCapturederrorCapturedonErrorCaptured错误捕获

二、Vue 2 选项式 API 生命周期详解

2.1 创建阶段

beforeCreate

javascript
export default {
  beforeCreate() {
    console.log("beforeCreate: 实例创建前");
    console.log("data:", this.$data); // undefined
    console.log("el:", this.$el); // undefined
    console.log("methods:", this.$options.methods); // 可访问
  },
};

特点

  • 实例刚创建,数据观测和初始化尚未开始
  • 无法访问 datacomputedmethods
  • 可用于初始化非响应式数据

created

javascript
export default {
  data() {
    return {
      message: "Hello Vue",
    };
  },
  created() {
    console.log("created: 实例创建完成");
    console.log("data:", this.$data); // { message: 'Hello Vue' }
    console.log("message:", this.message); // 'Hello Vue'
    console.log("el:", this.$el); // undefined

    // 适合发起异步请求
    this.fetchData();
  },
  methods: {
    fetchData() {
      // 发起 API 请求
    },
  },
};

特点

  • 实例创建完成,数据已初始化
  • 可访问 datacomputedmethods
  • $el 尚未生成,无法操作 DOM
  • 最佳实践:发起异步请求、初始化状态

2.2 挂载阶段

beforeMount

javascript
export default {
  beforeMount() {
    console.log("beforeMount: DOM 挂载前");
    console.log("el:", this.$el); // 已生成但未挂载
    console.log("document.body:", document.body.contains(this.$el)); // false
  },
};

特点

  • 模板已编译,$el 已生成
  • DOM 尚未挂载到页面
  • 可用于修改 $el 但不推荐

mounted

javascript
export default {
  mounted() {
    console.log("mounted: DOM 挂载完成");
    console.log("el:", this.$el); // 已挂载到 DOM
    console.log("document.body:", document.body.contains(this.$el)); // true

    // 操作 DOM
    this.$el.style.color = "red";

    // 初始化第三方库
    this.initChart();
  },
  methods: {
    initChart() {
      // 初始化图表库
    },
  },
};

特点

  • DOM 已挂载到页面
  • 可访问和操作 DOM
  • 最佳实践:初始化第三方库、DOM 操作、添加事件监听

2.3 更新阶段

beforeUpdate

javascript
export default {
  data() {
    return {
      count: 0,
    };
  },
  beforeUpdate() {
    console.log("beforeUpdate: 更新前");
    console.log("count:", this.count); // 更新后的值
    console.log("DOM count:", this.$el.textContent); // 更新前的 DOM 值
  },
  methods: {
    increment() {
      this.count++;
    },
  },
};

特点

  • 响应式数据已更新
  • DOM 尚未重新渲染
  • 可访问更新后的状态和更新前的 DOM
  • 避免在此时修改数据(会导致无限循环)

updated

javascript
export default {
  data() {
    return {
      items: [],
    };
  },
  updated() {
    console.log("updated: 更新完成");
    console.log("DOM updated:", this.$el.textContent); // 更新后的 DOM 值

    // 响应 DOM 更新
    this.scrollToBottom();
  },
  methods: {
    scrollToBottom() {
      // 滚动到底部
    },
  },
};

特点

  • DOM 已重新渲染完成
  • 可访问更新后的 DOM
  • 最佳实践:响应 DOM 更新、调整滚动位置

2.4 销毁阶段

beforeDestroy

javascript
export default {
  data() {
    return {
      timer: null,
    };
  },
  mounted() {
    this.timer = setInterval(() => {
      console.log("tick");
    }, 1000);
  },
  beforeDestroy() {
    console.log("beforeDestroy: 销毁前");
    console.log("timer:", this.timer); // 仍可访问

    // 清理定时器
    clearInterval(this.timer);

    // 移除事件监听
    window.removeEventListener("resize", this.handleResize);
  },
  methods: {
    handleResize() {
      // 处理窗口大小变化
    },
  },
};

特点

  • 实例即将被销毁
  • 仍可访问所有属性和方法
  • 最佳实践:清理定时器、移除事件监听、取消订阅

destroyed

javascript
export default {
  destroyed() {
    console.log("destroyed: 销毁完成");
    console.log("instance:", this); // 实例已销毁

    // 所有事件监听已自动移除
    // 所有子组件已销毁
  },
};

特点

  • 实例已完全销毁
  • 所有事件监听已自动移除
  • 所有子组件已销毁

三、Vue 3 组合式 API 生命周期详解

3.1 setup 函数

javascript
import { ref, onMounted, onUnmounted } from "vue";

export default {
  setup() {
    const count = ref(0);

    // setup 函数等效于 beforeCreate + created
    console.log("setup: 等同于 beforeCreate + created");
    console.log("count:", count.value); // 可访问

    // 注意:setup 中没有 this
    // 需要通过参数获取 props 和 context
    return {
      count,
    };
  },
};

特点

  • setupbeforeCreate 之前执行
  • 没有 this,需要通过参数获取上下文
  • 可访问响应式数据和方法
  • 最佳实践:初始化响应式数据、注册生命周期钩子

3.2 组合式 API 生命周期钩子

javascript
import {
  ref,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
} from "vue";

export default {
  setup() {
    const message = ref("Hello Vue 3");

    // DOM 挂载前
    onBeforeMount(() => {
      console.log("onBeforeMount: DOM 挂载前");
    });

    // DOM 挂载完成
    onMounted(() => {
      console.log("onMounted: DOM 挂载完成");
      // 操作 DOM
    });

    // 更新前
    onBeforeUpdate(() => {
      console.log("onBeforeUpdate: 更新前");
    });

    // 更新完成
    onUpdated(() => {
      console.log("onUpdated: 更新完成");
    });

    // 销毁前
    onBeforeUnmount(() => {
      console.log("onBeforeUnmount: 销毁前");
      // 清理资源
    });

    // 销毁完成
    onUnmounted(() => {
      console.log("onUnmounted: 销毁完成");
    });

    return {
      message,
    };
  },
};

3.3 组合式 API 实践案例

javascript
import { ref, onMounted, onUnmounted } from "vue";

export default {
  setup() {
    const windowWidth = ref(window.innerWidth);
    const windowHeight = ref(window.innerHeight);

    const handleResize = () => {
      windowWidth.value = window.innerWidth;
      windowHeight.value = window.innerHeight;
    };

    onMounted(() => {
      window.addEventListener("resize", handleResize);
      console.log("Mounted: 添加 resize 监听");
    });

    onUnmounted(() => {
      window.removeEventListener("resize", handleResize);
      console.log("Unmounted: 移除 resize 监听");
    });

    return {
      windowWidth,
      windowHeight,
    };
  },
};

四、父子组件生命周期顺序

4.1 创建阶段顺序

父组件 beforeCreate
父组件 created
父组件 beforeMount
  └── 子组件 beforeCreate
  └── 子组件 created
  └── 子组件 beforeMount
  └── 子组件 mounted
父组件 mounted

4.2 更新阶段顺序

父组件 beforeUpdate
  └── 子组件 beforeUpdate
  └── 子组件 updated
父组件 updated

4.3 销毁阶段顺序

父组件 beforeDestroy
  └── 子组件 beforeDestroy
  └── 子组件 destroyed
父组件 destroyed

4.4 代码验证

javascript
// 父组件
export default {
  beforeCreate() { console.log('Parent: beforeCreate') },
  created() { console.log('Parent: created') },
  beforeMount() { console.log('Parent: beforeMount') },
  mounted() { console.log('Parent: mounted') },
  beforeUpdate() { console.log('Parent: beforeUpdate') },
  updated() { console.log('Parent: updated') },
  beforeDestroy() { console.log('Parent: beforeDestroy') },
  destroyed() { console.log('Parent: destroyed') }
}

// 子组件
export default {
  beforeCreate() { console.log('Child: beforeCreate') },
  created() { console.log('Child: created') },
  beforeMount() { console.log('Child: beforeMount') },
  mounted() { console.log('Child: mounted') },
  beforeUpdate() { console.log('Child: beforeUpdate') },
  updated() { console.log('Child: updated') },
  beforeDestroy() { console.log('Child: beforeDestroy') },
  destroyed() { console.log('Child: destroyed') }
}

五、keep-alive 相关生命周期

5.1 activated 和 deactivated

javascript
// Vue 2 选项式 API
export default {
  activated() {
    console.log('activated: 组件被激活')
    // 恢复状态
    this.resumeData()
  },
  deactivated() {
    console.log('deactivated: 组件被失活')
    // 保存状态
    this.saveData()
  }
}

// Vue 3 组合式 API
import { onActivated, onDeactivated } from 'vue'

export default {
  setup() {
    onActivated(() => {
      console.log('onActivated: 组件被激活')
    })

    onDeactivated(() => {
      console.log('onDeactivated: 组件被失活')
    })
  }
}

5.2 keep-alive 使用场景

vue
<template>
  <div>
    <keep-alive>
      <component :is="currentComponent"></component>
    </keep-alive>
  </div>
</template>

<script>
export default {
  data() {
    return {
      currentComponent: "ComponentA",
      scrollPosition: 0,
    };
  },
  components: {
    ComponentA,
    ComponentB,
  },
  activated() {
    // 恢复滚动位置
    window.scrollTo(0, this.scrollPosition);
  },
  deactivated() {
    // 保存滚动位置
    this.scrollPosition = window.scrollY;
  },
};
</script>

六、常见踩坑与问题排查

6.1 在 beforeMount 中操作 DOM

javascript
// ❌ 错误:beforeMount 中无法访问已挂载的 DOM
export default {
  beforeMount() {
    // $el 已生成但尚未挂载到页面
    this.$el.querySelector('.my-class').style.color = 'red' // 可能报错
  }
}

// ✅ 正确:在 mounted 中操作 DOM
export default {
  mounted() {
    this.$el.querySelector('.my-class').style.color = 'red' // 安全
  }
}

6.2 在 updated 中修改数据导致无限循环

javascript
// ❌ 错误:在 updated 中修改数据会导致无限循环
export default {
  data() {
    return { count: 0 }
  },
  updated() {
    this.count++ // 触发更新 → updated → this.count++ → 无限循环
  }
}

// ✅ 正确:使用条件判断或 watch
export default {
  data() {
    return { count: 0, targetCount: 10 }
  },
  watch: {
    count(newVal) {
      if (newVal < this.targetCount) {
        this.count++
      }
    }
  }
}

6.3 忘记清理定时器和事件监听

javascript
// ❌ 错误:忘记清理定时器,组件销毁后仍在运行
export default {
  mounted() {
    this.timer = setInterval(() => {
      console.log('tick')
    }, 1000)
  }
}

// ✅ 正确:在 beforeDestroy 中清理
export default {
  data() {
    return { timer: null }
  },
  mounted() {
    this.timer = setInterval(() => {
      console.log('tick')
    }, 1000)
  },
  beforeDestroy() {
    clearInterval(this.timer)
  }
}

6.4 异步请求在组件销毁后返回

javascript
// ❌ 错误:组件销毁后,异步请求返回导致报错
export default {
  mounted() {
    this.fetchData()
  },
  methods: {
    async fetchData() {
      const result = await api.getData()
      this.data = result // 组件销毁后执行会报错
    }
  }
}

// ✅ 正确:使用取消令牌或标志位
export default {
  data() {
    return {
      data: null,
      isMounted: false
    }
  },
  mounted() {
    this.isMounted = true
    this.fetchData()
  },
  beforeDestroy() {
    this.isMounted = false
  },
  methods: {
    async fetchData() {
      const result = await api.getData()
      if (this.isMounted) {
        this.data = result
      }
    }
  }
}

6.5 Vue 3 中使用 this

javascript
// ❌ 错误:setup 中没有 this
export default {
  setup() {
    this.message = 'Hello' // 错误!this 是 undefined
  }
}

// ✅ 正确:使用响应式数据
import { ref } from 'vue'

export default {
  setup() {
    const message = ref('Hello')
    return { message }
  }
}

七、优化方案与进阶拓展

7.1 使用 watch 替代 updated

javascript
// ❌ 不推荐:updated 会在每次更新时触发
export default {
  updated() {
    if (this.someData) {
      this.doSomething()
    }
  }
}

// ✅ 推荐:使用 watch 精确监听
export default {
  watch: {
    someData(newVal) {
      if (newVal) {
        this.doSomething()
      }
    }
  }
}

// Vue 3 组合式 API
import { watch } from 'vue'

export default {
  setup() {
    const someData = ref(null)

    watch(someData, (newVal) => {
      if (newVal) {
        doSomething()
      }
    })
  }
}

7.2 使用 nextTick 确保 DOM 更新

javascript
// 场景:修改数据后立即操作 DOM
export default {
  data() {
    return { message: 'Hello' }
  },
  methods: {
    updateMessage() {
      this.message = 'Updated'

      // DOM 尚未更新
      console.log(this.$el.textContent) // 'Hello'

      // 使用 nextTick 等待 DOM 更新
      this.$nextTick(() => {
        console.log(this.$el.textContent) // 'Updated'
      })
    }
  }
}

// Vue 3
import { nextTick } from 'vue'

export default {
  setup() {
    const message = ref('Hello')

    const updateMessage = async () => {
      message.value = 'Updated'

      // DOM 尚未更新
      await nextTick()

      // DOM 已更新
    }
  }
}

7.3 使用 mixin 复用生命周期逻辑

javascript
// 创建可复用的生命周期 mixin
const resizeMixin = {
  data() {
    return {
      windowWidth: window.innerWidth,
      windowHeight: window.innerHeight,
    };
  },
  mounted() {
    window.addEventListener("resize", this.handleResize);
  },
  beforeDestroy() {
    window.removeEventListener("resize", this.handleResize);
  },
  methods: {
    handleResize() {
      this.windowWidth = window.innerWidth;
      this.windowHeight = window.innerHeight;
    },
  },
};

// 在组件中使用
export default {
  mixins: [resizeMixin],
  mounted() {
    // 调用父级 mounted
    this.$options.mounted.forEach((hook) => hook.call(this));

    // 组件自身逻辑
    console.log("Component mounted");
  },
};

7.4 Vue 3 组合式 API 封装

javascript
// 封装可复用的逻辑
import { ref, onMounted, onUnmounted } from "vue";

export function useWindowSize() {
  const width = ref(window.innerWidth);
  const height = ref(window.innerHeight);

  const handleResize = () => {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  };

  onMounted(() => {
    window.addEventListener("resize", handleResize);
  });

  onUnmounted(() => {
    window.removeEventListener("resize", handleResize);
  });

  return {
    width,
    height,
  };
}

// 在组件中使用
import { useWindowSize } from "../composables";

export default {
  setup() {
    const { width, height } = useWindowSize();

    return {
      width,
      height,
    };
  },
};

八、全文总结

Vue 生命周期钩子是理解 Vue 组件工作原理的关键:

  1. 创建阶段beforeCreatecreatedbeforeMountmounted
  2. 更新阶段beforeUpdateupdated
  3. 销毁阶段beforeDestroydestroyed
  4. Vue 3 变化setup 替代 beforeCreatecreated,其他钩子通过 onXxx 函数注册
  5. 父子顺序:父组件先创建、后销毁;子组件后创建、先销毁
  6. 最佳实践
    • created 中发起异步请求
    • mounted 中操作 DOM 和初始化第三方库
    • beforeDestroy 中清理资源(定时器、事件监听)
    • 使用 nextTick 确保 DOM 更新后再操作
    • Vue 3 使用组合式 API 封装可复用逻辑