Vue.js: 组件生命周期

最后更新:2026-08-26

组件生命周期是 Vue 组件从创建到销毁的完整过程:实例化 → 挂载到 DOM → 数据更新 → 卸载。Vue 提供了 8 个生命周期钩子让你在特定时刻执行代码。

掌握生命周期是写"会呼吸"的组件的关键——你可以在挂载时加载数据,在更新时做副作用,在卸载时清理资源。

1. 你将学到


2. 一个数据加载组件的"闪烁"问题

(1) 痛点:组件渲染了,但数据还没加载

Alice 构建了一个用户资料组件:

VUE
<!-- ❌ 翻车版:数据未到就渲染 -->
<template>
  <div>
    <h1>{{ user.name }}</h1>
    <p>{{ user.email }}</p>
  </div>
</template>

<script setup>
const { ref } = Vue

const user = ref({})  // 空对象,渲染时显示 "undefined"
fetch('/api/user')
  .then(res => res.json())
  .then(data => user.value = data)
</script>

用户体验:

产品经理 Charlie:

"Alice,用户看到 'undefined' 整整半秒。我们需要 loading 状态。数据到达之前先显示'Loading...'。"

(2) Vue 生命周期 + loading 状态解法

VUE
<template>
  <div>
    <p v-if="loading">Loading...</p>
    <div v-else>
      <h1>{{ user.name }}</h1>
      <p>{{ user.email }}</p>
    </div>
  </div>
</template>

<script setup>
const { ref, onMounted } = Vue

const user = ref(null)
const loading = ref(true)

// ✅ 在 onMounted 中加载数据(DOM 已就绪,loading 状态会先显示)
onMounted(async () => {
  const res = await fetch('/api/user')
  user.value = await res.json()
  loading.value = false
})
</script>

用户体验:

(3) 收益

加上生命周期后:


3. 完整生命周期流程图

100%
graph TB
    A[创建组件实例] --> B[setup()]
    B --> C[setup 响应式]
    C --> D[setup()]
    D --> E[模板编译]
    E --> F[onBeforeMount]
    F --> G[挂载到 DOM]
    G --> H[onMounted]
    H --> I{数据更新?}
    I -->|是| J[onBeforeUpdate]
    J --> K[重新渲染]
    K --> L[onUpdated]
    L --> I
    I -->|否| M{组件卸载?}
    M -->|是| N[onBeforeUnmount]
    N --> O[卸载]
    O --> P[onUnmounted]
    
    style H fill:#42b883,color:#fff
    style L fill:#42b883,color:#fff
    style P fill:#42b883,color:#fff

4. 8 个生命周期钩子详解

(1) 8 大钩子速查

钩子 触发时机 常用场景 频率
onBeforeMount 组件挂载前 准备数据
onMounted 组件挂载后 加载数据、DOM 操作 ⭐⭐⭐⭐⭐
onBeforeUpdate 数据更新前 性能优化 ⭐⭐
onUpdated 数据更新后 DOM 重新渲染后操作 ⭐⭐⭐
onBeforeUnmount 组件卸载前 清理定时器 ⭐⭐⭐
onUnmounted 组件卸载后 最终清理 ⭐⭐
onErrorCaptured 捕获子组件错误 错误边界 ⭐⭐⭐
onActivated keep-alive 激活 缓存组件恢复 ⭐⭐
onDeactivated keep-alive 停用 缓存组件停用 ⭐⭐

(2) onMounted:最常用

VUE
<script setup>
const { ref, onMounted } = Vue

const user = ref(null)

onMounted(async () => {
  // 1. 加载初始数据
  const res = await fetch('/api/user')
  user.value = await res.json()
  
  // 2. DOM 操作(操作真实 DOM)
  document.title = `${user.value.name} - Dashboard`
  
  // 3. 注册全局事件监听
  const handleResize = () => { console.log('Window resized') }
window.addEventListener('resize', handleResize)
  
  // 4. 设置定时器
  const timer = setInterval(() => {
    console.log('Tick')
  }, 1000)
  
  // ❌ 注意:onUnmounted 中要清理 timer 和 listener
})
</script>

(3) onUnmounted:清理资源

VUE
<script setup>
const { onMounted, onUnmounted } = Vue

let timer = null

onMounted(() => {
  timer = setInterval(() => console.log('Tick'), 1000)
  const handleResize = () => { console.log('Window resized') }
window.addEventListener('resize', handleResize)
})

// ✅ 清理:避免内存泄漏
onUnmounted(() => {
  clearInterval(timer)
  window.removeEventListener('resize', handleResize)
})
</script>

(4) onErrorCaptured:错误边界

VUE
<!-- ErrorBoundary.vue 父组件 -->
<script setup>
const { onErrorCaptured, ref } = Vue

const error = ref(null)

onErrorCaptured((err, instance, info) => {
  console.error('Caught error:', err)
  console.log('Component:', instance)
  console.log('Info:', info)
  
  error.value = err.message
  return false  // 阻止错误继续向上传播
})
</script>

<template>
  <div>
    <p v-if="error" class="error">Error: {{ error }}</p>
    <slot v-else />
  </div>
</template>
VUE
<!-- App.vue 使用 -->
<ErrorBoundary>
  <UserProfile :user-id="123" />  <!-- 如果出错,会被 ErrorBoundary 捕获 -->
</ErrorBoundary>

(5) 5 大使用场景

场景 钩子 代码
加载初始数据 onMounted await fetch(...)
设置定时器 onMounted + onUnmounted setInterval + clearInterval
监听全局事件 onMounted + onUnmounted addEventListener + removeEventListener
DOM 操作 onMounted document.querySelector(...)
第三方库集成 onMounted + onUnmounted new Chart(...) + chart.destroy()

5. 父子组件生命周期执行顺序

(1) 挂载顺序

100%
sequenceDiagram
    participant P as Parent
    participant C as Child
    
    P->>P: 1. Parent onBeforeMount
    P->>P: 2. Parent onMounted
    C->>C: 3. Child onBeforeMount
    C->>C: 4. Child onMounted
    
    Note over P,C: ❌ 这是错的:父 mounted 应该在子之后

正确顺序

100%
sequenceDiagram
    participant P as Parent
    participant C as Child
    
    P->>P: 1. Parent onBeforeMount
    C->>C: 2. Child onBeforeMount
    C->>C: 3. Child onMounted
    P->>P: 4. Parent onMounted
    
    Note over P,C: ✅ 父组件等所有子组件挂载完才挂载

(2) 完整执行顺序(嵌套组件)

TEXT 📖 仅展示
1. Parent setup()
2. Parent setup
3. Parent setup()
4. Parent onBeforeMount
5. Child setup()
6. Child setup
7. Child setup()
8. Child onBeforeMount
9. Child onMounted
10. Parent onMounted

(3) 卸载顺序(反过来)

TEXT 📖 仅展示
1. Parent onBeforeUnmount
2. Child onBeforeUnmount
3. Child onUnmounted
4. Parent onUnmounted

6. keep-alive 缓存组件

(1) 什么是 keep-alive?

<keep-alive> 缓存组件实例,避免重复创建/销毁。常用在 Tab 切换、路由切换场景。

VUE
<!-- 父组件 -->
<template>
  <button v-for="tab in tabs" :key="tab" @click="currentTab = tab">
    {{ tab }}
  </button>
  
  <!-- keep-alive 包裹:切换 Tab 时不销毁组件 -->
  <keep-alive>
    <component :is="currentTabComponent" />
  </keep-alive>
</template>

(2) 2 个新增钩子

VUE
<!-- 子组件:被 keep-alive 缓存 -->
<script setup>
const { onActivated, onDeactivated } = Vue

// 组件被激活时(从缓存中恢复)
onActivated(() => {
  console.log('Component activated')
  // 重新加载数据、恢复滚动位置等
})

// 组件被停用时(切走但保留在缓存)
onDeactivated(() => {
  console.log('Component deactivated')
  // 保存状态、暂停定时器等
})
</script>

(3) 5 大使用场景

场景 做法
Tab 切换 <keep-alive> 包裹 Tab 内容
路由切换 <keep-alive> 包裹 <router-view>
弹窗缓存 <keep-alive> 包裹弹窗
列表页 缓存已加载的列表
表单页 缓存未提交的表单

7. 完整示例:用户资料组件

▶ 示例:onMounted + onUnmounted(数据加载 + 清理)

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <button @click="showUser = !showUser">{{ showUser ? '卸载' : '挂载' }}组件</button>
  <user-profile v-if="showUser" :user-id="1"></user-profile>
</div>

<script>
const { createApp, ref, onMounted, onUnmounted } = Vue

// 子组件:UserProfile
const UserProfile = {
  props: ['userId'],
  setup(props) {
    const user = ref(null)
    const loading = ref(true)
    let timer = null

    // 模拟 fetch(避免跨域)
    function mockFetch(id) {
      return new Promise(resolve => {
        setTimeout(() => {
          resolve({ id, name: 'Alice ' + id, email: `alice${id}@example.com` })
        }, 500)
      })
    }

    onMounted(async () => {
      // 1. 加载数据
      const data = await mockFetch(props.userId)
      user.value = data
      loading.value = false
      console.log('✅ Mounted: 数据已加载')

      // 2. 设置定时器
      timer = setInterval(() => {
        console.log('⏰ Timer tick')
      }, 5000)
    })

    onUnmounted(() => {
      // ✅ 清理:避免内存泄漏
      clearInterval(timer)
      console.log('🧹 Unmounted: 已清理定时器')
    })

    return { user, loading }
  },
  template: `
    <div style="padding: 1rem; border: 1px solid #ddd; margin: 0.5rem 0;">
      <p v-if="loading">Loading...</p>
      <div v-else-if="user">
        <h1>{{ user.name }}</h1>
        <p>{{ user.email }}</p>
      </div>
    </div>
  `
}

const App = {
  components: { UserProfile },
  setup() {
    const showUser = ref(true)
    return { showUser }
  }
}

createApp(App).mount('#app')
</script>
逻辑代码 54 行(超过 40 行限制,仅展示)

▶ 示例:8 个钩子使用速查

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <button @click="count++">count++ ({{ count }})</button>
  <button @click="show = !show">{{ show ? '卸载' : '挂载' }}</button>
  <lifecycle-demo v-if="show" :count="count"></lifecycle-demo>
  <p style="color: #999; font-size: 0.85rem;">查看 console 输出</p>
</div>

<script>
const { createApp, ref, onBeforeMount, onMounted, onBeforeUpdate,
        onUpdated, onBeforeUnmount, onUnmounted,
        onErrorCaptured, onActivated, onDeactivated } = Vue

const LifecycleDemo = {
  props: ['count'],
  setup() {
    onBeforeMount(() => console.log('1. onBeforeMount: 准备挂载'))
    onMounted(() => console.log('2. onMounted: 已挂载'))
    onBeforeUpdate(() => console.log('3. onBeforeUpdate: 即将更新'))
    onUpdated(() => console.log('4. onUpdated: 已更新'))
    onBeforeUnmount(() => console.log('5. onBeforeUnmount: 准备卸载'))
    onUnmounted(() => console.log('6. onUnmounted: 已卸载'))
    onErrorCaptured((err) => { console.error('7. onErrorCaptured:', err); return false })
    onActivated(() => console.log('8. onActivated: keep-alive 激活'))
    onDeactivated(() => console.log('9. onDeactivated: keep-alive 停用'))
    return {}
  },
  template: '<div style="padding: 1rem; background: #f0f9ff;">Props count: {{ count }}</div>'
}

const App = {
  components: { LifecycleDemo },
  setup() {
    const count = ref(0)
    const show = ref(true)
    return { count, show }
  }
}

createApp(App).mount('#app')
</script>
▶ 试一试

▶ 示例:父子组件生命周期顺序(console 输出)

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <p style="color: #999; font-size: 0.85rem;">查看 console:先 Child 后 Parent</p>
  <parent-demo></parent-demo>
</div>

<script>
const { createApp, onMounted } = Vue

const ChildDemo = {
  setup() {
    onMounted(() => console.log('3. Child onMounted'))
    return {}
  },
  template: '<div style="padding: 0.5rem; background: #fef3c7;">Child 子组件</div>'
}

const ParentDemo = {
  components: { ChildDemo },
  setup() {
    onMounted(() => console.log('4. Parent onMounted(父等子挂载完)'))
    return {}
  },
  template: `
    <div style="border: 1px solid #ddd; padding: 1rem;">
      <p>Parent 父组件</p>
      <child-demo></child-demo>
    </div>
  `
}

createApp(ParentDemo).mount('#app')
</script>
▶ 试一试

▶ 示例:onErrorCaptured 错误边界

HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <error-boundary>
    <buggy-component></buggy-component>
  </error-boundary>
</div>

<script>
const { createApp, ref, onErrorCaptured } = Vue

// 会出错的子组件
const BuggyComponent = {
  setup() {
    // 模拟错误:访问 undefined.value
    const data = ref(null)
    setTimeout(() => { data.value.foo = 'bar' }, 100)
    return { data }
  },
  template: '<div>{{ data.foo }}</div>'
}

// 错误边界
const ErrorBoundary = {
  components: { BuggyComponent },
  setup() {
    const error = ref(null)
    onErrorCaptured((err, instance, info) => {
      console.error('Caught:', err.message)
      console.log('Info:', info)
      error.value = err.message
      return false   // 阻止向上传播
    })
    return { error }
  },
  template: `
    <div>
      <div v-if="error" style="padding: 1rem; background: #fee; color: #c00;">
        ⚠️ Error: {{ error }}
      </div>
      <slot v-else></slot>
    </div>
  `
}

const App = { components: { ErrorBoundary } }
createApp(App).mount('#app')
</script>
▶ 试一试

▶ 示例:keep-alive 缓存组件

HTML 📖 仅展示
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>

<div id="app">
  <button
    v-for="tab in tabs" :key="tab"
    :style="{ padding: '6px 12px', margin: '2px', border: 'none', background: currentTab === tab ? '#42b883' : '#eee', color: currentTab === tab ? 'white' : '#333', cursor: 'pointer' }"
    @click="currentTab = tab"
  >{{ tab }}</button>

  <keep-alive>
    <component :is="currentTabComponent"></component>
  </keep-alive>
</div>

<script>
const { createApp, ref, computed, onActivated, onDeactivated, onMounted } = Vue

// 3 个 Tab 子组件
const Home = {
  setup() {
    onMounted(() => console.log('🏠 Home mounted'))
    onActivated(() => console.log('🏠 Home activated'))
    onDeactivated(() => console.log('🏠 Home deactivated'))
    return { counter: ref(0) }
  },
  template: `
    <div style="padding: 1rem; background: #ecfdf5;">
      <h3>Home</h3>
      <button @click="$data.counter = ($data.counter || 0) + 1">内部计数: {{ $data.counter || 0 }}</button>
      <p style="color: #999; font-size: 0.85rem;">切换到其他 Tab 再切回,计数保留</p>
    </div>
  `
}

const Profile = {
  setup() {
    onMounted(() => console.log('👤 Profile mounted'))
    onActivated(() => console.log('👤 Profile activated'))
    onDeactivated(() => console.log('👤 Profile deactivated'))
    return {}
  },
  template: '<div style="padding: 1rem; background: #eff6ff;"><h3>Profile</h3></div>'
}

const Settings = {
  setup() {
    onMounted(() => console.log('⚙️ Settings mounted'))
    onActivated(() => console.log('⚙️ Settings activated'))
    onDeactivated(() => console.log('⚙️ Settings deactivated'))
    return {}
  },
  template: '<div style="padding: 1rem; background: #fef3c7;"><h3>Settings</h3></div>'
}

const App = {
  components: { Home, Profile, Settings },
  setup() {
    const tabs = ['Home', 'Profile', 'Settings']
    const currentTab = ref('Home')
    const currentTabComponent = computed(() =>
      ({ Home, Profile, Settings })[currentTab.value]
    )
    return { tabs, currentTab, currentTabComponent }
  }
}

createApp(App).mount('#app')
</script>
逻辑代码 59 行(超过 40 行限制,仅展示)

▶ 示例:5 个常见错误速查

错误 现象 解决
在 setup 顶层用 document 服务端报错 用 onMounted
没清理定时器 内存泄漏 onUnmounted 中 clear
onMounted 中改 props Vue 警告 改用 emit
异步组件不处理错误 静默失败 onErrorCaptured
父子 mounted 顺序错乱 调试困惑 理解"父等子"原则

❓ 常见问题

Q onMounted 和 setup() 区别?
A setup() 时组件已创建但 DOM 未生成(不能操作 DOM)。onMounted 时 DOM 已挂载(可操作 DOM)。加载数据一般用 onMounted(避免 SSR 问题)。
Q 生命周期钩子能异步吗?
A 能。onMounted(async () => { await fetch(...) }) 是合法写法。Vue 不等异步完成才继续。
Q keep-alive 的 onActivated 和 onMounted 区别?
A onMounted 只触发 1 次(首次挂载)。onActivated 每次从缓存恢复都触发。Tab 切换会触发 onActivated + onDeactivated(不是 onMounted + onUnmounted)。
Q onErrorCaptured 能捕获哪些错误?
A 能捕获 3 类:(1) 子组件渲染错误;(2) 子组件生命周期钩子错误;(3) 子组件 watch 回调错误。但不能捕获自身错误和异步错误。
Q 父组件能监听子组件的生命周期吗?
A 能。用 @hook 监听:@hook:mounted="handleChildMounted"。但推荐用 props/emit 通信,不直接监听生命周期。
Q Vue 2 的生命周期钩子和 Vue 3 区别?
A Vue 3 重命名为 onMounted(Vue 2 是 mounted),并新增 onErrorCaptured 和 onActivated/onDeactivated。Vue 2 钩子在 Vue 3 都可用(向后兼容)。
Q SSR(服务端渲染)时生命周期会怎样?
A onBeforeMount / onMounted / onBeforeUpdate / onUpdated 不会在服务端触发(无 DOM)。只有 setup() / setup() / onBeforeUnmount / onUnmounted / onErrorCaptured 触发。

📖 小节


📝 作业

  1. 基础题(难度⭐) 实现一个简单的 UserCard 组件:

    • onMounted 加载用户数据(mock 1s 延迟)
    • 显示 loading → 显示用户信息
    • onUnmounted 打印 "Component destroyed"
  2. 进阶题(难度⭐⭐) 实现一个 Timer 组件:

    • onMounted 启动定时器(每秒 +1)
    • 显示当前秒数
    • onUnmounted 清理定时器
    • 加个按钮可以暂停/恢复(用 ref 存状态)
  3. 挑战题(难度⭐⭐⭐) 实现一个完整的 Tab 系统:

    1. TabContainer.vue:3 个 Tab(Home/Profile/Settings)
    2. 3 个 Tab 内容组件
    3. <keep-alive> 缓存组件
    4. 切换 Tab 时输出 "X activated, Y deactivated"
    5. 在其中一个 Tab 用 onMounted 加载数据
    6. 切走时不卸载(因为 keep-alive),切回时数据还在
    7. 用 onErrorCaptured 捕获子组件错误
Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏