Skip to content

Vue.js 生命周期

什么是生命周期?

生命周期是指 Vue 组件从创建到销毁的整个过程。在这个过程中,Vue 提供了一系列钩子函数,让我们可以在特定阶段执行代码。

Vue 3 生命周期钩子

组合式 API 生命周期钩子

在 Vue 3 的 <script setup> 中,生命周期钩子需要从 Vue 中导入:

vue
<script setup>
import { 
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted
} from 'vue'

onMounted(() => {
  console.log('组件已挂载')
})
</script>

生命周期图示

创建阶段

setup() - 组件创建时执行

onBeforeMount() - 挂载前

onMounted() - 挂载后(可以访问 DOM)

更新阶段(数据变化时)

onBeforeUpdate() - 更新前

onUpdated() - 更新后

销毁阶段

onBeforeUnmount() - 卸载前

onUnmounted() - 卸载后

各个生命周期钩子详解

onMounted(挂载后)

组件挂载到 DOM 后调用,此时可以访问 DOM 元素。

vue
<script setup>
import { ref, onMounted } from 'vue'

const message = ref('Hello')
const elementRef = ref(null)

onMounted(() => {
  console.log('组件已挂载')
  console.log('DOM 元素:', elementRef.value)
  
  // 适合进行:
  // - DOM 操作
  // - 发起 API 请求
  // - 初始化第三方库
})
</script>

<template>
  <div ref="elementRef">{{ message }}</div>
</template>

onBeforeMount(挂载前)

在组件挂载到 DOM 之前调用。

vue
<script setup>
import { onBeforeMount } from 'vue'

onBeforeMount(() => {
  console.log('即将挂载组件')
  // 此时还不能访问 DOM
})
</script>

onUpdated(更新后)

响应式数据变化导致 DOM 更新后调用。

vue
<script setup>
import { ref, onUpdated } from 'vue'

const count = ref(0)

onUpdated(() => {
  console.log('组件已更新,count:', count.value)
  // 注意:避免在这里修改状态,可能导致无限循环
})
</script>

<template>
  <div>
    <p>计数:{{ count }}</p>
    <button @click="count++">增加</button>
  </div>
</template>

onBeforeUpdate(更新前)

在响应式数据变化后、DOM 更新前调用。

vue
<script setup>
import { ref, onBeforeUpdate } from 'vue'

const count = ref(0)

onBeforeUpdate(() => {
  console.log('即将更新 DOM')
})
</script>

onUnmounted(卸载后)

组件卸载后调用,用于清理工作。

vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

let timer = null

onMounted(() => {
  timer = setInterval(() => {
    console.log('定时器运行中...')
  }, 1000)
})

onUnmounted(() => {
  console.log('组件已卸载')
  // 清理定时器
  if (timer) {
    clearInterval(timer)
  }
  
  // 适合进行:
  // - 清理定时器
  // - 取消事件监听
  // - 取消网络请求
})
</script>

onBeforeUnmount(卸载前)

组件卸载前调用。

vue
<script setup>
import { onBeforeUnmount } from 'vue'

onBeforeUnmount(() => {
  console.log('即将卸载组件')
  // 可以在这里进行一些清理工作
})
</script>

实战示例:数据加载

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const data = ref(null)
const loading = ref(true)
const error = ref(null)
let abortController = null

async function fetchData() {
  loading.value = true
  error.value = null
  
  // 创建 AbortController 用于取消请求
  abortController = new AbortController()
  
  try {
    const response = await fetch('https://api.example.com/data', {
      signal: abortController.signal
    })
    data.value = await response.json()
  } catch (err) {
    if (err.name !== 'AbortError') {
      error.value = err.message
    }
  } finally {
    loading.value = false
  }
}

onMounted(() => {
  console.log('组件挂载,开始加载数据')
  fetchData()
})

onUnmounted(() => {
  console.log('组件卸载,取消请求')
  // 取消未完成的请求
  if (abortController) {
    abortController.abort()
  }
})
</script>

<template>
  <div class="data-container">
    <div v-if="loading" class="loading">
      <div class="spinner"></div>
      <p>加载中...</p>
    </div>
    
    <div v-else-if="error" class="error">
      <p>❌ 加载失败:{{ error }}</p>
      <button @click="fetchData">重试</button>
    </div>
    
    <div v-else class="content">
      <pre>{{ data }}</pre>
    </div>
  </div>
</template>

<style scoped>
.data-container {
  padding: 20px;
  max-width: 600px;
  margin: 0 auto;
}

.loading {
  text-align: center;
  padding: 40px;
}

.spinner {
  width: 40px;
  height: 40px;
  margin: 0 auto 20px;
  border: 4px solid #f3f3f3;
  border-top: 4px solid #42b983;
  border-radius: 50%;
  animation: spin 1s linear infinite;
}

@keyframes spin {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}

.error {
  text-align: center;
  color: #f56c6c;
  padding: 20px;
}

button {
  padding: 8px 16px;
  background-color: #42b983;
  color: white;
  border: none;
  border-radius: 4px;
  cursor: pointer;
}
</style>

实战示例:定时器

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const currentTime = ref(new Date())
const count = ref(0)
let timeInterval = null
let countInterval = null

onMounted(() => {
  console.log('启动定时器')
  
  // 更新时间
  timeInterval = setInterval(() => {
    currentTime.value = new Date()
  }, 1000)
  
  // 计数器
  countInterval = setInterval(() => {
    count.value++
  }, 1000)
})

onUnmounted(() => {
  console.log('清理定时器')
  
  if (timeInterval) {
    clearInterval(timeInterval)
  }
  
  if (countInterval) {
    clearInterval(countInterval)
  }
})

function formatTime(date) {
  return date.toLocaleTimeString('zh-CN')
}
</script>

<template>
  <div class="timer-container">
    <h2>实时时钟</h2>
    <div class="time">{{ formatTime(currentTime) }}</div>
    <div class="count">运行时间:{{ count }} 秒</div>
  </div>
</template>

<style scoped>
.timer-container {
  text-align: center;
  padding: 40px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 12px;
  max-width: 400px;
  margin: 20px auto;
}

.time {
  font-size: 48px;
  font-weight: bold;
  margin: 20px 0;
  font-family: 'Courier New', monospace;
}

.count {
  font-size: 18px;
  opacity: 0.9;
}
</style>

实战示例:事件监听

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const windowWidth = ref(window.innerWidth)
const windowHeight = ref(window.innerHeight)
const mouseX = ref(0)
const mouseY = ref(0)
const scrollY = ref(0)

function handleResize() {
  windowWidth.value = window.innerWidth
  windowHeight.value = window.innerHeight
}

function handleMouseMove(event) {
  mouseX.value = event.clientX
  mouseY.value = event.clientY
}

function handleScroll() {
  scrollY.value = window.scrollY
}

onMounted(() => {
  console.log('添加事件监听')
  window.addEventListener('resize', handleResize)
  window.addEventListener('mousemove', handleMouseMove)
  window.addEventListener('scroll', handleScroll)
})

onUnmounted(() => {
  console.log('移除事件监听')
  window.removeEventListener('resize', handleResize)
  window.removeEventListener('mousemove', handleMouseMove)
  window.removeEventListener('scroll', handleScroll)
})
</script>

<template>
  <div class="event-monitor">
    <h2>事件监控</h2>
    
    <div class="info-grid">
      <div class="info-card">
        <h3>窗口尺寸</h3>
        <p>{{ windowWidth }} × {{ windowHeight }}</p>
      </div>
      
      <div class="info-card">
        <h3>鼠标位置</h3>
        <p>X: {{ mouseX }}, Y: {{ mouseY }}</p>
      </div>
      
      <div class="info-card">
        <h3>滚动位置</h3>
        <p>{{ scrollY }}px</p>
      </div>
    </div>
  </div>
</template>

<style scoped>
.event-monitor {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.info-grid {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
  gap: 20px;
  margin-top: 20px;
}

.info-card {
  padding: 20px;
  background-color: #f5f5f5;
  border-radius: 8px;
  text-align: center;
}

.info-card h3 {
  margin: 0 0 10px 0;
  color: #42b983;
}

.info-card p {
  font-size: 18px;
  font-weight: bold;
  margin: 0;
}
</style>

实战示例:第三方库集成

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

const chartContainer = ref(null)
let chartInstance = null

onMounted(() => {
  console.log('初始化图表')
  
  // 模拟初始化第三方图表库
  if (chartContainer.value) {
    // 实际使用时,这里会是 Chart.js、ECharts 等库的初始化代码
    chartInstance = {
      render: () => {
        chartContainer.value.innerHTML = `
          <div style="padding: 40px; text-align: center; background: #f0f0f0; border-radius: 8px;">
            <h3>图表已初始化</h3>
            <p>这里会显示实际的图表</p>
          </div>
        `
      },
      destroy: () => {
        console.log('销毁图表实例')
      }
    }
    
    chartInstance.render()
  }
})

onUnmounted(() => {
  console.log('清理图表')
  
  // 销毁图表实例,释放资源
  if (chartInstance) {
    chartInstance.destroy()
    chartInstance = null
  }
})
</script>

<template>
  <div class="chart-wrapper">
    <h2>图表示例</h2>
    <div ref="chartContainer" class="chart-container"></div>
  </div>
</template>

<style scoped>
.chart-wrapper {
  padding: 20px;
  max-width: 800px;
  margin: 0 auto;
}

.chart-container {
  min-height: 400px;
  margin-top: 20px;
}
</style>

生命周期最佳实践

1. 在 onMounted 中进行 DOM 操作

vue
<script setup>
import { ref, onMounted } from 'vue'

const inputRef = ref(null)

onMounted(() => {
  // ✅ 正确:在 onMounted 中访问 DOM
  inputRef.value?.focus()
})

// ❌ 错误:在 setup 中直接访问 DOM(此时 DOM 还未创建)
// inputRef.value?.focus()
</script>

<template>
  <input ref="inputRef" placeholder="自动聚焦" />
</template>

2. 清理副作用

vue
<script setup>
import { onMounted, onUnmounted } from 'vue'

let subscription = null

onMounted(() => {
  // 订阅某个服务
  subscription = someService.subscribe(data => {
    console.log(data)
  })
})

onUnmounted(() => {
  // ✅ 清理订阅
  if (subscription) {
    subscription.unsubscribe()
  }
})
</script>

3. 避免在 onUpdated 中修改状态

vue
<script setup>
import { ref, onUpdated } from 'vue'

const count = ref(0)

onUpdated(() => {
  // ❌ 错误:可能导致无限循环
  // count.value++
  
  // ✅ 正确:只读取状态或执行不会触发更新的操作
  console.log('count 已更新:', count.value)
})
</script>

4. 使用组合式函数封装生命周期逻辑

vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'

// 可复用的组合式函数
function useWindowSize() {
  const width = ref(window.innerWidth)
  const height = ref(window.innerHeight)
  
  function update() {
    width.value = window.innerWidth
    height.value = window.innerHeight
  }
  
  onMounted(() => {
    window.addEventListener('resize', update)
  })
  
  onUnmounted(() => {
    window.removeEventListener('resize', update)
  })
  
  return { width, height }
}

// 使用组合式函数
const { width, height } = useWindowSize()
</script>

<template>
  <div>窗口尺寸:{{ width }} × {{ height }}</div>
</template>

选项式 API vs 组合式 API

选项式 API(Vue 2 风格)

vue
<script>
export default {
  data() {
    return {
      count: 0
    }
  },
  beforeCreate() {
    console.log('beforeCreate')
  },
  created() {
    console.log('created')
  },
  beforeMount() {
    console.log('beforeMount')
  },
  mounted() {
    console.log('mounted')
  },
  beforeUpdate() {
    console.log('beforeUpdate')
  },
  updated() {
    console.log('updated')
  },
  beforeUnmount() {
    console.log('beforeUnmount')
  },
  unmounted() {
    console.log('unmounted')
  }
}
</script>

组合式 API(Vue 3 推荐)

vue
<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUpdated, onBeforeUnmount, onUnmounted } from 'vue'

const count = ref(0)

// setup() 相当于 beforeCreate 和 created
console.log('setup')

onBeforeMount(() => console.log('onBeforeMount'))
onMounted(() => console.log('onMounted'))
onBeforeUpdate(() => console.log('onBeforeUpdate'))
onUpdated(() => console.log('onUpdated'))
onBeforeUnmount(() => console.log('onBeforeUnmount'))
onUnmounted(() => console.log('onUnmounted'))
</script>

总结

  • onMounted:组件挂载后,适合 DOM 操作、API 请求、初始化第三方库
  • onUnmounted:组件卸载后,适合清理定时器、事件监听、取消请求
  • onUpdated:数据更新后,避免在此修改状态
  • setup():相当于 beforeCreate 和 created
  • 使用组合式函数封装可复用的生命周期逻辑
  • 始终记得清理副作用,避免内存泄漏

下一步

接下来学习 动态组件,了解如何动态切换组件。